41
96
of the current file (i.e., the filename with its extension
42
97
stripped). This is used to generate filenames for images."""
99
COPY_CLIPBOARD_JS = ''
102
######################################################################
104
######################################################################
106
def read_ref_file(basename=None):
107
if basename is None: basename = OUTPUT_BASENAME
108
if not os.path.exists(basename + REF_EXTENSION):
109
warning('File %r does not exist!' %
110
(basename + REF_EXTENSION))
111
return dict(targets=(),terms={},reference_labes={})
112
f = open(basename + REF_EXTENSION)
113
ref_info = pickle.load(f)
117
def write_ref_file(ref_info):
118
f = open(OUTPUT_BASENAME + REF_EXTENSION, 'w')
119
pickle.dump(ref_info, f)
122
def add_to_ref_file(**ref_info):
123
if os.path.exists(OUTPUT_BASENAME + REF_EXTENSION):
124
info = read_ref_file()
125
info.update(ref_info)
128
write_ref_file(ref_info)
130
######################################################################
132
######################################################################
134
class example(docutils.nodes.paragraph): pass
136
def example_directive(name, arguments, options, content, lineno,
137
content_offset, block_text, state, state_machine):
141
.. example:: John went to the store.
143
To refer to examples, use::
146
.. example:: John went to the store.
148
In store_, John performed an action.
150
text = '\n'.join(content)
152
state.nested_parse(content, content_offset, node)
154
example_directive.content = True
155
directives.register_directive('example', example_directive)
156
directives.register_directive('ex', example_directive)
158
def doctest_directive(name, arguments, options, content, lineno,
159
content_offset, block_text, state, state_machine):
161
Used to explicitly mark as doctest blocks things that otherwise
162
wouldn't look like doctest blocks.
164
text = '\n'.join(content)
165
if re.match(r'.*\n\s*\n', block_text):
166
warning('doctest-ignore on line %d will not be ignored, '
167
'because there is\na blank line between ".. doctest-ignore::"'
168
' and the doctest example.' % lineno)
169
return [docutils.nodes.doctest_block(text, text, codeblock=True)]
170
doctest_directive.content = True
171
directives.register_directive('doctest-ignore', doctest_directive)
174
def tree_directive(name, arguments, options, content, lineno,
175
content_offset, block_text, state, state_machine):
177
text = '\n'.join(arguments) + '\n'.join(content)
179
# Note: the two filenames generated by these two cases should be
180
# different, to prevent conflicts.
181
if OUTPUT_FORMAT == 'latex':
182
density, scale = 300, 150
183
scale = scale * options.get('scale', 100) / 100
184
filename = '%s-tree-%s.pdf' % (OUTPUT_BASENAME, _treenum)
185
align = LATEX_VALIGN_IS_BROKEN and 'bottom' or 'top'
186
elif OUTPUT_FORMAT == 'html':
187
density, scale = 100, 100
188
density = density * options.get('scale', 100) / 100
189
filename = '%s-tree-%s.png' % (OUTPUT_BASENAME, _treenum)
191
elif OUTPUT_FORMAT == 'ref':
194
assert 0, 'bad output format %r' % OUTPUT_FORMAT
195
if not os.path.exists(TREE_IMAGE_DIR):
196
os.mkdir(TREE_IMAGE_DIR)
198
filename = os.path.join(TREE_IMAGE_DIR, filename)
199
tree_to_image(text, filename, density)
202
warning('Error parsing tree: %s\n%s\n%s' % (e, text, filename))
203
return [example(text, text)]
205
imagenode = docutils.nodes.image(uri=filename, scale=scale, align=align)
208
tree_directive.arguments = (1,0,1)
209
tree_directive.content = True
210
tree_directive.options = {'scale': directives.nonnegative_int}
211
directives.register_directive('tree', tree_directive)
213
def avm_directive(name, arguments, options, content, lineno,
214
content_offset, block_text, state, state_machine):
215
text = '\n'.join(content)
217
if OUTPUT_FORMAT == 'latex':
218
latex_avm = parse_avm(textwrap.dedent(text)).as_latex()
219
return [docutils.nodes.paragraph('','',
220
docutils.nodes.raw('', latex_avm, format='latex'))]
221
elif OUTPUT_FORMAT == 'html':
222
return [parse_avm(textwrap.dedent(text)).as_table()]
223
elif OUTPUT_FORMAT == 'ref':
224
return [docutils.nodes.paragraph()]
225
except ValueError, e:
226
if isinstance(e.args[0], int):
227
warning('Error parsing avm on line %s' % (lineno+e.args[0]))
230
warning('Error parsing avm on line %s: %s' % (lineno, e))
231
node = example(text, text)
232
state.nested_parse(content, content_offset, node)
234
avm_directive.content = True
235
directives.register_directive('avm', avm_directive)
237
def def_directive(name, arguments, options, content, lineno,
238
content_offset, block_text, state, state_machine):
239
state_machine.document.setdefault('__defs__', {})[arguments[0]] = 1
241
def_directive.arguments = (1, 0, 0)
242
directives.register_directive('def', def_directive)
244
def ifdef_directive(name, arguments, options, content, lineno,
245
content_offset, block_text, state, state_machine):
246
if arguments[0] in state_machine.document.get('__defs__', ()):
247
node = docutils.nodes.compound('')
248
state.nested_parse(content, content_offset, node)
252
ifdef_directive.arguments = (1, 0, 0)
253
ifdef_directive.content = True
254
directives.register_directive('ifdef', ifdef_directive)
256
def ifndef_directive(name, arguments, options, content, lineno,
257
content_offset, block_text, state, state_machine):
258
if arguments[0] not in state_machine.document.get('__defs__', ()):
259
node = docutils.nodes.compound('')
260
state.nested_parse(content, content_offset, node)
264
ifndef_directive.arguments = (1, 0, 0)
265
ifndef_directive.content = True
266
directives.register_directive('ifndef', ifndef_directive)
269
######################################################################
271
######################################################################
273
def table_directive(name, arguments, options, content, lineno,
274
content_offset, block_text, state, state_machine):
275
# The identifier for this table.
277
table_id = arguments[0]
278
if table_id in _table_ids:
279
warning("Duplicate table id %r" % table_id)
280
_table_ids.add(table_id)
282
# Create a target element for the table
283
target = docutils.nodes.target(names=[table_id])
284
state_machine.document.note_explicit_target(target)
286
# Parse the contents.
287
node = docutils.nodes.compound('')
288
state.nested_parse(content, content_offset, node)
289
if len(node) == 0 or not isinstance(node[0], docutils.nodes.table):
290
return [state_machine.reporter.error(
291
'Error in "%s" directive: expected table as first child' %
294
# Move the caption into the table.
296
caption = docutils.nodes.caption('','', *node[1:])
297
table.append(caption)
299
# Return the target and the table.
301
return [target, table]
306
table_directive.arguments = (0,1,0) # 1 optional arg, no whitespace
307
table_directive.content = True
308
table_directive.options = {'caption': directives.unchanged}
309
directives.register_directive('table', table_directive)
312
######################################################################
314
######################################################################
315
# We define a new attribute for doctest blocks: 'is_codeblock'. If
316
# this attribute is true, then the block contains python code only
317
# (i.e., don't expect to find prompts.)
319
class pylisting(docutils.nodes.General, docutils.nodes.Element):
321
Python source code listing.
323
Children: doctest_block+ caption?
325
class callout_marker(docutils.nodes.Inline, docutils.nodes.Element):
327
A callout marker for doctest block. This element contains no
328
children; and defines the attribute 'number'.
331
DOCTEST_BLOCK_RE = re.compile('((?:[ ]*>>>.*\n?(?:.*[^ ].*\n?)+\s*)+)',
333
CALLOUT_RE = re.compile(r'#[ ]+\[_([\w-]+)\][ ]*$', re.MULTILINE)
335
from docutils.nodes import fully_normalize_name as normalize_name
338
def pylisting_directive(name, arguments, options, content, lineno,
339
content_offset, block_text, state, state_machine):
340
# The identifier for this listing.
341
listing_id = arguments[0]
342
if listing_id in _listing_ids:
343
warning("Duplicate listing id %r" % listing_id)
344
_listing_ids.add(listing_id)
346
# Create the pylisting element itself.
347
listing = pylisting('\n'.join(content), name=listing_id, callouts={})
349
# Create a target element for the pylisting.
350
target = docutils.nodes.target(names=[listing_id])
351
state_machine.document.note_explicit_target(target)
353
# Divide the text into doctest blocks.
354
for i, v in enumerate(DOCTEST_BLOCK_RE.split('\n'.join(content))):
355
pysrc = re.sub(r'\A( *\n)+', '', v.rstrip())
357
listing.append(docutils.nodes.doctest_block(pysrc, pysrc,
358
is_codeblock=(i%2==0)))
360
# Add an optional caption.
361
if options.get('caption'):
362
cap = options['caption'].split('\n')
363
caption = docutils.nodes.compound()
364
state.nested_parse(docutils.statemachine.StringList(cap),
365
content_offset, caption)
366
if (len(caption) == 1 and isinstance(caption[0],
367
docutils.nodes.paragraph)):
368
listing.append(docutils.nodes.caption('', '', *caption[0]))
370
warning("Caption should be a single paragraph")
371
listing.append(docutils.nodes.caption('', '', *caption))
373
return [target, listing]
375
pylisting_directive.arguments = (1,0,0) # 1 required arg, no whitespace
376
pylisting_directive.content = True
377
pylisting_directive.options = {'caption': directives.unchanged}
378
directives.register_directive('pylisting', pylisting_directive)
380
def callout_directive(name, arguments, options, content, lineno,
381
content_offset, block_text, state, state_machine):
383
prefix = '%s-' % arguments[0]
386
node = docutils.nodes.compound('')
387
state.nested_parse(content, content_offset, node)
388
if not (len(node.children) == 1 and
389
isinstance(node[0], docutils.nodes.field_list)):
390
return [state_machine.reporter.error(
391
'Error in "%s" directive: may contain a single defintion '
392
'list only.' % (name), line=lineno)]
394
node[0]['classes'] = ['callouts']
395
for field in node[0]:
396
if len(field[0]) != 1:
397
return [state_machine.reporter.error(
398
'Error in "%s" directive: bad field id' % (name), line=lineno)]
400
field_name = prefix+('%s' % field[0][0])
402
field[0].append(docutils.nodes.reference(field_name, field_name,
404
field[0]['classes'] = ['callout']
408
callout_directive.arguments = (0,1,0) # 1 optional arg, no whitespace
409
callout_directive.content = True
410
directives.register_directive('callouts', callout_directive)
412
_OPTION_DIRECTIVE_RE = re.compile(
413
r'(\n[ ]*\.\.\.[ ]*)?#\s*doctest:\s*([^\n\'"]*)$', re.MULTILINE)
414
def strip_doctest_directives(text):
415
return _OPTION_DIRECTIVE_RE.sub('', text)
418
######################################################################
420
######################################################################
422
def rst_example_directive(name, arguments, options, content, lineno,
423
content_offset, block_text, state, state_machine):
424
raw = docutils.nodes.literal_block('', '\n'.join(content))
425
out = docutils.nodes.compound('')
426
state.nested_parse(content, content_offset, out)
427
if OUTPUT_FORMAT == 'latex':
429
docutils.nodes.definition_list('',
430
docutils.nodes.definition_list_item('',
431
docutils.nodes.term('','Input'),
432
docutils.nodes.definition('', raw)),
433
docutils.nodes.definition_list_item('',
434
docutils.nodes.term('','Rendered'),
435
docutils.nodes.definition('', out)))]
438
docutils.nodes.table('',
439
docutils.nodes.tgroup('',
440
docutils.nodes.colspec(colwidth=5,classes=['rst-raw']),
441
docutils.nodes.colspec(colwidth=5),
442
docutils.nodes.thead('',
443
docutils.nodes.row('',
444
docutils.nodes.entry('',
445
docutils.nodes.paragraph('','Input')),
446
docutils.nodes.entry('',
447
docutils.nodes.paragraph('','Rendered')))),
448
docutils.nodes.tbody('',
449
docutils.nodes.row('',
450
docutils.nodes.entry('',raw),
451
docutils.nodes.entry('',out)))),
452
classes=["rst-example"])]
454
rst_example_directive.arguments = (0, 0, 0)
455
rst_example_directive.content = True
456
directives.register_directive('rst_example', rst_example_directive)
459
######################################################################
461
######################################################################
465
This | is | used | to | make | aligned | glosses.
466
NN | BE | VB | TO | VB | JJ | NN
470
class gloss(docutils.nodes.Element): "glossrow+"
471
class glossrow(docutils.nodes.Element): "paragraph+"
473
def gloss_directive(name, arguments, options, content, lineno,
474
content_offset, block_text, state, state_machine):
475
# Transform into a table.
476
lines = list(content)
477
maxlen = max(len(line) for line in lines)
478
lines = [('|%-'+`maxlen`+'s|') % line for line in lines]
481
for line in (lines+['']):
482
div = ['-']*(maxlen+2)
483
for m in re.finditer(r'\|', prevline):
485
for m in re.finditer(r'\|', line):
487
tablestr += ''.join(div) + '\n' + line + '\n'
489
table_lines = tablestr.strip().split('\n')
490
new_content = docutils.statemachine.StringList(table_lines)
491
# [XX] DEBUG GLOSSES:
492
# print 'converted to:'
496
node = docutils.nodes.compound('')
497
state.nested_parse(new_content, content_offset, node)
498
if not (len(node.children) == 1 and
499
isinstance(node[0], docutils.nodes.table)):
500
error = state_machine.reporter.error(
501
'Error in "%s" directive: may contain a single table '
502
'only.' % (name), line=lineno)
505
table['classes'] = ['gloss', 'nolines']
508
for colspec in colspecs:
509
colspec['colwidth'] = colspec.get('colwidth',4)/2
511
return [example('', '', table)]
512
gloss_directive.arguments = (0, 0, 0)
513
gloss_directive.content = True
514
directives.register_directive('gloss', gloss_directive)
517
######################################################################
519
######################################################################
521
class Citations(Transform):
522
default_priority = 500 # before footnotes.
524
if not os.path.exists(BIBTEX_FILE):
525
warning('Warning bibtex file %r not found. '
526
'Not linking citations.' % BIBTEX_FILE)
528
bibliography = self.read_bibinfo(BIBTEX_FILE)
529
for k, citation_refs in self.document.citation_refs.items():
530
for citation_ref in citation_refs[:]:
531
cite = bibliography.get(citation_ref['refname'].lower())
533
new_cite = self.citeref(cite, citation_ref['refname'])
534
citation_ref.replace_self(new_cite)
535
self.document.citation_refs[k].remove(citation_ref)
537
def citeref(self, cite, key):
538
if LOCAL_BIBLIOGRAPHY:
539
return docutils.nodes.raw('', '\cite{%s}' % key, format='latex')
541
return docutils.nodes.reference('', '', docutils.nodes.Text(cite),
542
refuri='%s#%s' % (BIBLIOGRAPHY_HTML, key))
544
BIB_ENTRY = re.compile(r'@\w+{.*')
545
def read_bibinfo(self, filename):
546
bibliography = {} # key -> authors, year
548
for line in open(filename):
551
# @InProceedings{<key>,
552
m = re.match(r'@\w+{([^,]+),$', line)
554
key = m.group(1).strip().lower()
555
bibliography[key] = [None, None]
557
# author = <authors>,
558
m = re.match(r'(?i)author\s*=\s*(.*)$', line)
560
bibliography[key][0] = self.bib_authors(m.group(1))
562
m = re.match(r'(?i)editor\s*=\s*(.*)$', line)
564
bibliography[key][0] = self.bib_authors(m.group(1))
567
m = re.match(r'(?i)year\s*=\s*(.*)$', line)
569
bibliography[key][1] = self.bib_year(m.group(1))
570
for key in bibliography:
571
if bibliography[key][0] is None: warning('no author found:', key)
572
if bibliography[key][1] is None: warning('no year found:', key)
573
bibliography[key] = '[%s, %s]' % tuple(bibliography[key])
574
#debug('%20s %s' % (key, `bibliography[key]`))
577
def bib_year(self, year):
578
return re.sub(r'["\'{},]', "", year)
580
def bib_authors(self, authors):
581
# Strip trailing comma:
582
if authors[-1:] == ',': authors=authors[:-1]
583
# Strip quotes or braces:
584
authors = re.sub(r'"(.*)"$', r'\1', authors)
585
authors = re.sub(r'{(.*)}$', r'\1', authors)
586
authors = re.sub(r"'(.*)'$", r'\1', authors)
588
authors = re.split(r'\s+and\s+', authors)
589
# Keep last name only:
590
authors = [a.split()[-1] for a in authors]
592
if len(authors) == 1:
594
elif len(authors) == 2:
595
return '%s & %s' % tuple(authors)
596
elif len(authors) == 3:
597
return '%s, %s, & %s' % tuple(authors)
599
return '%s et al' % authors[0]
603
######################################################################
605
######################################################################
606
class termdef(docutils.nodes.Inline, docutils.nodes.TextElement): pass
607
class idxterm(docutils.nodes.Inline, docutils.nodes.TextElement): pass
608
class index(docutils.nodes.Element): pass
610
def idxterm_role(name, rawtext, text, lineno, inliner,
611
options={}, content=[]):
612
if name == 'dt': options['classes'] = ['termdef']
613
elif name == 'topic': options['classes'] = ['topic']
614
else: options['classes'] = ['term']
615
# Recursively parse the contents of the index term, in case it
616
# contains a substitiution (like |alpha|).
617
nodes, msgs = inliner.parse(text, lineno, memo=inliner,
618
parent=inliner.parent)
619
return [idxterm(rawtext, '', *nodes, **options)], []
621
roles.register_canonical_role('dt', idxterm_role)
622
roles.register_canonical_role('idx', idxterm_role)
623
roles.register_canonical_role('topic', idxterm_role)
625
def index_directive(name, arguments, options, content, lineno,
626
content_offset, block_text, state, state_machine):
627
pending = docutils.nodes.pending(ConstructIndex)
628
pending.details.update(options)
629
state_machine.document.note_pending(pending)
630
return [index('', pending)]
631
index_directive.arguments = (0, 0, 0)
632
index_directive.content = False
633
index_directive.options = {'extern': directives.flag}
634
directives.register_directive('index', index_directive)
637
class SaveIndexTerms(Transform):
638
default_priority = 810 # before NumberReferences transform
640
v = FindTermVisitor(self.document)
641
self.document.walkabout(v)
643
if OUTPUT_FORMAT == 'ref':
644
add_to_ref_file(terms=v.terms)
646
class ConstructIndex(Transform):
647
default_priority = 820 # after NumberNodes, before NumberReferences.
649
# Find any indexed terms in this document.
650
v = FindTermVisitor(self.document)
651
self.document.walkabout(v)
654
# Check the extern reference files for additional terms.
655
if 'extern' in self.startnode.details:
656
for filename in EXTERN_REFERENCE_FILES:
657
basename = os.path.splitext(filename)[0]
658
terms.update(read_ref_file(basename)['terms'])
660
# Build the index & insert it into the document.
661
index_node = self.build_index(terms)
662
self.startnode.replace_self(index_node)
664
def build_index(self, terms):
665
if not terms: return []
667
top = docutils.nodes.bullet_list('', classes=['index'])
671
for key in sorted(terms.keys()):
672
if key[:1] != start_letter:
673
top.append(docutils.nodes.list_item(
674
'', docutils.nodes.paragraph('', key[:1].upper()+'\n',
675
classes=['index-heading']),
676
docutils.nodes.bullet_list('', classes=['index-section']),
678
section = top[-1][-1]
679
section.append(self.entry(terms[key]))
680
start_letter = key[:1]
684
def entry(self, term_info):
685
entrytext, name, sectnum = term_info
686
if sectnum is not None:
687
entrytext.append(docutils.nodes.emphasis('', ' (%s)' % sectnum))
688
ref = docutils.nodes.reference('', '', refid=name,
691
para = docutils.nodes.paragraph('', '', ref)
692
return docutils.nodes.list_item('', para, classes=['index'])
694
class FindTermVisitor(docutils.nodes.SparseNodeVisitor):
695
def __init__(self, document):
697
docutils.nodes.NodeVisitor.__init__(self, document)
698
def unknown_visit(self, node): pass
699
def unknown_departure(self, node): pass
701
def visit_idxterm(self, node):
702
node['name'] = node['id'] = self.idxterm_key(node)
703
node['names'] = node['ids'] = [node['id']]
704
container = self.container_section(node)
706
entrytext = node.deepcopy()
707
if container: sectnum = container.get('sectnum')
710
self.terms[node['name']] = (entrytext, name, sectnum)
712
def idxterm_key(self, node):
713
key = re.sub('\W', '_', node.astext().lower())+'_index_term'
714
if key not in self.terms: return key
716
while '%s_%d' % (key, n) in self.terms: n += 1
717
return '%s_%d' % (key, n)
719
def container_section(self, node):
720
while not isinstance(node, docutils.nodes.section):
721
if node.parent is None: return None
722
else: node = node.parent
727
######################################################################
729
######################################################################
731
class ResolveExternalCrossrefs(Transform):
733
Using the information from EXTERN_REFERENCE_FILES, look for any
734
links to external targets, and set their `refuid` appropriately.
735
Also, if they are a figure, section, table, or example, then
736
replace the link of the text with the appropriate counter.
738
default_priority = 849 # right before dangling refs
741
ref_dict = self.build_ref_dict()
742
v = ExternalCrossrefVisitor(self.document, ref_dict)
743
self.document.walkabout(v)
745
def build_ref_dict(self):
746
"""{target -> (uri, label)}"""
748
for filename in EXTERN_REFERENCE_FILES:
749
basename = os.path.splitext(filename)[0]
750
if OUTPUT_FORMAT == 'html':
751
uri = os.path.split(basename)[-1]+'.html'
753
uri = os.path.split(basename)[-1]+'.pdf'
754
if basename == OUTPUT_BASENAME:
755
pass # don't read our own ref file.
756
elif not os.path.exists(basename+REF_EXTENSION):
757
warning('%s does not exist' % (basename+REF_EXTENSION))
759
ref_info = read_ref_file(basename)
760
for ref in ref_info['targets']:
761
label = ref_info['reference_labels'].get(ref)
762
ref_dict[ref] = (uri, label)
766
class ExternalCrossrefVisitor(docutils.nodes.NodeVisitor):
767
def __init__(self, document, ref_dict):
768
docutils.nodes.NodeVisitor.__init__(self, document)
769
self.ref_dict = ref_dict
770
def unknown_visit(self, node): pass
771
def unknown_departure(self, node): pass
773
# Don't mess with the table of contents.
774
def visit_topic(self, node):
775
if 'contents' in node.get('classes', ()):
776
raise docutils.nodes.SkipNode
778
def visit_reference(self, node):
779
if node.resolved: return
780
node_id = node.get('refid') or node.get('refname')
781
if node_id in self.ref_dict:
782
uri, label = self.ref_dict[node_id]
783
#debug('xref: %20s -> %-30s (label=%s)' % (
784
# node_id, uri+'#'+node_id, label))
785
node['refuri'] = '%s#%s' % (uri, node_id)
788
if label is not None:
789
if node.get('expanded_ref'):
790
warning('Label %s is defined both locally (%s) and '
791
'externally (%s)' % (node_id, node[0], label))
795
node.append(docutils.nodes.Text(label))
796
expand_reference_text(node)
798
######################################################################
800
######################################################################
803
.. exercise:: path.xml
806
class exercise(docutils.nodes.paragraph,docutils.nodes.Element): pass
808
def exercise_directive(name, arguments, options, content, lineno,
809
content_offset, block_text, state, state_machine):
810
return [exercise('', arguments[0])]
812
exercise_directive.arguments = (1, 0, 0)
813
exercise_directive.content = False
814
directives.register_directive('exercise', exercise_directive)
817
######################################################################
818
#{ Challenges (optional exercises; harder than usual)
819
######################################################################
822
.. challenge:: path.xml
825
class challenge(docutils.nodes.paragraph,docutils.nodes.Element): pass
827
def challenge_directive(name, arguments, options, content, lineno,
828
content_offset, block_text, state, state_machine):
829
return [challenge('', arguments[0])]
831
challenge_directive.arguments = (1, 0, 0)
832
challenge_directive.content = False
833
directives.register_directive('challenge', challenge_directive)
837
######################################################################
838
#{ Figure & Example Numbering
839
######################################################################
841
# [xx] number examples, figures, etc, relative to chapter? e.g.,
842
# figure 3.2? maybe number examples within-chapter, but then restart
845
class section_context(docutils.nodes.Invisible, docutils.nodes.Element):
846
def __init__(self, context):
847
docutils.nodes.Element.__init__(self, '', context=context)
848
assert self['context'] in ('body', 'preface', 'appendix')
850
def section_context_directive(name, arguments, options, content, lineno,
851
content_offset, block_text, state, state_machine):
852
return [section_context(name)]
853
section_context_directive.arguments = (0,0,0)
854
directives.register_directive('preface', section_context_directive)
855
directives.register_directive('body', section_context_directive)
856
directives.register_directive('appendix', section_context_directive)
858
class NumberNodes(Transform):
860
This transform adds numbers to figures, tables, and examples; and
861
converts references to the figures, tables, and examples to use
862
these numbers. For example, given the rst source::
865
.. ex:: John likes Mary.
867
See example my_example_.
869
This transform will assign a number to the example, '(1)', and
870
will replace the following text with 'see example (1)', with an
873
# dangling = 850; contents = 720.
874
default_priority = 800
876
v = NumberingVisitor(self.document)
877
self.document.walkabout(v)
878
self.document.reference_labels = v.reference_labels
879
self.document.callout_labels = v.callout_labels
881
class NumberReferences(Transform):
882
default_priority = 830
884
v = ReferenceVisitor(self.document, self.document.reference_labels,
885
self.document.callout_labels)
886
self.document.walkabout(v)
888
# Save reference info to a pickle file.
889
if OUTPUT_FORMAT == 'ref':
890
add_to_ref_file(reference_labels=self.document.reference_labels,
893
class NumberingVisitor(docutils.nodes.NodeVisitor):
895
A transforming visitor that adds figure numbers to all figures,
896
and converts any references to figures to use the text 'Figure #';
897
and adds example numbers to all examples, and converts any
898
references to examples to use the text 'Example #'.
900
LETTERS = 'abcdefghijklmnopqrstuvwxyz'
901
ROMAN = 'i ii iii iv v vi vii viii ix x'.split()
902
ROMAN += ['x%s' % r for r in ROMAN]
904
def __init__(self, document):
905
docutils.nodes.NodeVisitor.__init__(self, document)
906
self.reference_labels = {}
909
self.example_num = [0]
910
self.section_num = [0]
912
self.callout_labels = {} # name -> number
913
self.set_section_context = None
914
self.section_context = 'body' # preface, appendix, body
916
#////////////////////////////////////////////////////////////
918
#////////////////////////////////////////////////////////////
920
def visit_figure(self, node):
922
num = '%s.%s' % (self.format_section_num(1), self.figure_num)
923
for node_id in self.get_ids(node):
924
self.reference_labels[node_id] = '%s' % num
925
self.label_node(node, 'Figure %s' % num)
927
#////////////////////////////////////////////////////////////
929
#////////////////////////////////////////////////////////////
931
def visit_table(self, node):
932
if 'avm' in node['classes']: return
933
if 'gloss' in node['classes']: return
934
if 'rst-example' in node['classes']: return
935
if 'doctest-list' in node['classes']: return
937
num = '%s.%s' % (self.format_section_num(1), self.table_num)
938
for node_id in self.get_ids(node):
939
self.reference_labels[node_id] = '%s' % num
940
self.label_node(node, 'Table %s' % num)
942
#////////////////////////////////////////////////////////////
944
#////////////////////////////////////////////////////////////
946
def visit_pylisting(self, node):
947
self.listing_num += 1
948
num = '%s.%s' % (self.format_section_num(1), self.listing_num)
949
for node_id in self.get_ids(node):
950
self.reference_labels[node_id] = '%s' % num
951
pyfile = re.sub('\W', '_', node['name']) + PYLISTING_EXTENSION
952
self.label_node(node, 'Listing %s (%s)' % (num, pyfile),
953
PYLISTING_DIR + pyfile)
954
self.callout_labels.update(node['callouts'])
956
def visit_doctest_block(self, node):
957
if isinstance(node.parent, pylisting):
958
callouts = node['callouts'] = node.parent['callouts']
960
callouts = node['callouts'] = {}
962
pysrc = ''.join(('%s' % c) for c in node)
963
for callout_id in CALLOUT_RE.findall(pysrc):
964
callouts[callout_id] = len(callouts)+1
965
self.callout_labels.update(callouts)
967
#////////////////////////////////////////////////////////////
969
#////////////////////////////////////////////////////////////
970
max_section_depth = 3
971
no_section_numbers_in_preface = True
972
TOP_SECTION = 'chapter'
974
# [xx] I don't think this currently does anything..
975
def visit_document(self, node):
976
if (len(node)>0 and isinstance(node[0], docutils.nodes.title) and
977
isinstance(node[0].children[0], docutils.nodes.Text) and
978
re.match(r'(\d+(.\d+)*)\.?\s+', node[0].children[0].data)):
979
node['sectnum'] = node[0].children[0].data.split()[0]
980
for node_id in node.get('ids', []):
981
self.reference_labels[node_id] = '%s' % node['sectnum']
983
def visit_section(self, node):
986
# Check if we're entering a new context.
987
if len(self.section_num) == 1 and self.set_section_context:
988
self.start_new_context(node)
990
# Record the section's context in its title.
991
title['section_context'] = self.section_context
993
# Increment the section counter.
994
self.section_num[-1] += 1
996
# If a section number is given explicitly as part of the
997
# title, then it overrides our counter.
998
if isinstance(title.children[0], docutils.nodes.Text):
999
m = re.match(r'(\d+(.\d+)*)\.?\s+', title.children[0].data)
1001
pieces = [int(n) for n in m.group(1).split('.')]
1002
if len(pieces) == len(self.section_num):
1003
self.section_num = pieces
1004
title.children[0].data = title.children[0].data[m.end():]
1006
warning('Explicit section number (%s) does not match '
1007
'current section depth' % m.group(1))
1008
self.prepend_raw_latex(node, r'\setcounter{%s}{%d}' %
1009
(self.TOP_SECTION, self.section_num[0]-1))
1011
# Record the reference pointer for this section; and add the
1012
# section number to the section title.
1013
node['sectnum'] = self.format_section_num()
1014
for node_id in node.get('ids', []):
1015
self.reference_labels[node_id] = '%s' % node['sectnum']
1016
if (len(self.section_num) <= self.max_section_depth and
1017
(OUTPUT_FORMAT != 'latex') and
1018
not (self.section_context == 'preface' and
1019
self.no_section_numbers_in_preface)):
1020
label = docutils.nodes.generated('', node['sectnum']+u'\u00a0'*3,
1021
classes=['sectnum'])
1022
title.insert(0, label)
1025
# Record the section number.
1026
self.section_num.append(0)
1028
# If this was a top-level section, then restart the figure,
1029
# table, and listing counters
1030
if len(self.section_num) == 2:
1033
self.listing_num = 0
1035
def start_new_context(self,node):
1036
# Set the 'section_context' var.
1037
self.section_context = self.set_section_context
1038
self.set_section_context = None
1040
# Update our counter.
1041
self.section_num[0] = 0
1043
# Update latex's counter.
1044
if self.section_context == 'preface': style = 'Roman'
1045
elif self.section_context == 'body': style = 'arabic'
1046
elif self.section_context == 'appendix': style = 'Alph'
1047
raw_latex = (('\n'+r'\setcounter{%s}{0}' + '\n' +
1048
r'\renewcommand \the%s{\%s{%s}}'+'\n') %
1049
(self.TOP_SECTION, self.TOP_SECTION, style, self.TOP_SECTION))
1050
if self.section_context == 'appendix':
1051
raw_latex += '\\appendix\n'
1052
self.prepend_raw_latex(node, raw_latex)
1054
def prepend_raw_latex(self, node, raw_latex):
1055
if isinstance(node, docutils.nodes.document):
1056
node.insert(0, docutils.nodes.raw('', raw_latex, format='latex'))
1058
node_index = node.parent.children.index(node)
1059
node.parent.insert(node_index, docutils.nodes.raw('', raw_latex,
1062
def depart_section(self, node):
1063
self.section_num.pop()
1065
def format_section_num(self, depth=None):
1066
pieces = [('%s' % p) for p in self.section_num]
1067
if self.section_context == 'body':
1068
pieces[0] = ('%s' % self.section_num[0])
1069
elif self.section_context == 'preface':
1070
pieces[0] = self.ROMAN[self.section_num[0]-1].upper()
1071
elif self.section_context == 'appendix':
1072
pieces[0] = self.LETTERS[self.section_num[0]-1].upper()
1074
assert 0, 'unexpected section context'
1076
return '.'.join(pieces)
1078
return '.'.join(pieces[:depth])
1081
def visit_section_context(self, node):
1082
assert node['context'] in ('body', 'preface', 'appendix')
1083
self.set_section_context = node['context']
1084
node.replace_self([])
1086
#////////////////////////////////////////////////////////////
1088
#////////////////////////////////////////////////////////////
1089
NESTED_EXAMPLES = True
1091
def visit_example(self, node):
1092
self.example_num[-1] += 1
1093
node['num'] = self.short_example_num()
1094
for node_id in self.get_ids(node):
1095
self.reference_labels[node_id] = self.format_example_num()
1096
self.example_num.append(0)
1098
def depart_example(self, node):
1099
if not self.NESTED_EXAMPLES:
1100
if self.example_num[-1] > 0:
1101
# If the example contains a list of subexamples, then
1102
# splice them in to our parent.
1103
node.replace_self(list(node))
1104
self.example_num.pop()
1106
def short_example_num(self):
1107
if len(self.example_num) == 1:
1108
return '(%s)' % self.example_num[0]
1109
if len(self.example_num) == 2:
1110
return '%s.' % self.LETTERS[self.example_num[1]-1]
1111
if len(self.example_num) == 3:
1112
return '%s.' % self.ROMAN[self.example_num[2]-1]
1114
return '%s.' % self.example_num[-1]
1116
def format_example_num(self):
1117
""" (1), (2); (1a), (1b); (1a.i), (1a.ii)"""
1118
ex_num = ('%s' % self.example_num[0])
1119
if len(self.example_num) > 1:
1120
ex_num += self.LETTERS[self.example_num[1]-1]
1121
if len(self.example_num) > 2:
1122
ex_num += '.%s' % self.ROMAN[self.example_num[2]-1]
1123
for n in self.example_num[3:]:
1125
return '(%s)' % ex_num
1127
#////////////////////////////////////////////////////////////
1129
#////////////////////////////////////////////////////////////
1131
def unknown_visit(self, node): pass
1132
def unknown_departure(self, node): pass
1134
def get_ids(self, node):
1135
node_index = node.parent.children.index(node)
1136
if node_index>0 and isinstance(node.parent[node_index-1],
1137
docutils.nodes.target):
1138
target = node.parent[node_index-1]
1139
if target.has_key('refid'):
1140
refid = target['refid']
1141
target['ids'] = [refid]
1144
elif target.has_key('ids'):
1145
return target['ids']
1147
warning('unable to find id for %s' % target)
1151
def label_node(self, node, label, refuri=None, cls='caption-label'):
1152
if not isinstance(node[-1], docutils.nodes.caption):
1153
node.append(docutils.nodes.caption())
1156
if OUTPUT_FORMAT == 'html':
1157
cap = docutils.nodes.inline('', label, classes=[cls])
1159
cap = docutils.nodes.reference('', '', cap, refuri=refuri,
1160
mimetype='text/x-python')
1161
caption.insert(0, cap)
1162
if len(caption) > 1:
1163
caption.insert(1, docutils.nodes.Text(': '))
1165
class ReferenceVisitor(docutils.nodes.NodeVisitor):
1166
def __init__(self, document, reference_labels, callout_labels):
1167
self.reference_labels = reference_labels
1168
self.callout_labels = callout_labels
1169
self.targets = set()
1170
docutils.nodes.NodeVisitor.__init__(self, document)
1171
def unknown_visit(self, node):
1172
if isinstance(node, docutils.nodes.Element):
1173
self.targets.update(node.get('names', []))
1174
self.targets.update(node.get('ids', []))
1175
def unknown_departure(self, node): pass
1177
# Don't mess with the table of contents.
1178
def visit_topic(self, node):
1179
if 'contents' in node.get('classes', ()):
1180
raise docutils.nodes.SkipNode
1182
def visit_reference(self, node):
1183
node_id = (node.get('refid') or
1184
self.document.nameids.get(node.get('refname')) or
1185
node.get('refname'))
1186
if node_id in self.reference_labels:
1187
label = self.reference_labels[node_id]
1189
node.append(docutils.nodes.Text(label))
1190
expand_reference_text(node)
1191
elif node_id in self.callout_labels:
1192
label = self.callout_labels[node_id]
1194
node.append(callout_marker(number=label, name='ref-%s' % node_id))
1195
expand_reference_text(node)
1196
# There's no explicitly encoded target element, so manually
1197
# resolve the reference:
1198
node['refid'] = node_id
1199
node.resolved = True
1201
_EXPAND_REF_RE = re.compile(r'(?is)^(.*)(%s)\s+$' % '|'.join(
1202
['figure', 'table', 'example', 'chapter', 'section', 'appendix',
1203
'sentence', 'tree', 'listing', 'program']))
1204
def expand_reference_text(node):
1205
"""If the reference is immediately preceeded by the word 'figure'
1206
or the word 'table' or 'example', then include that word in the
1207
link (rather than just the number)."""
1208
if node.get('expanded_ref'):
1209
assert 0, ('Already expanded!! %s' % node)
1210
node_index = node.parent.children.index(node)
1212
prev_node = node.parent.children[node_index-1]
1213
if (isinstance(prev_node, docutils.nodes.Text)):
1214
m = _EXPAND_REF_RE.match(prev_node.data)
1216
prev_node.data = m.group(1)
1217
link = node.children[0]
1218
link.data = '%s %s' % (m.group(2), link.data)
1219
node['expanded_ref'] = True
1221
######################################################################
1222
#{ Feature Structures (AVMs)
1223
######################################################################
1226
def __init__(self, ident):
1230
def assign(self, key, val):
1231
if key in self.keys: raise ValueError('duplicate key')
1232
self.keys.append(key)
1233
self.vals[key] = val
1236
for key in self.keys:
1237
val = self.vals[key]
1238
if isinstance(val, AVMPointer):
1239
vals.append('%s -> %s' % (key, val.ident))
1241
vals.append('%s = %s' % (key, val))
1242
s = '{%s}' % ', '.join(vals)
1243
if self.ident: s += '[%s]' % self.ident
1247
return '\\begin{avm}\n%s\\end{avm}\n' % self._as_latex()
1249
def _as_latex(self, indent=0):
1250
if self.ident: ident = '\\@%s ' % self.ident
1252
lines = ['%s %s & %s' % (indent*' ', key,
1253
self.vals[key]._as_latex(indent+1))
1254
for key in self.keys]
1255
return ident + '\\[\n' + ' \\\\\n'.join(lines) + '\\]\n'
1257
def _entry(self, val, cls):
1258
if isinstance(val, basestring):
1259
return docutils.nodes.entry('',
1260
docutils.nodes.paragraph('', val), classes=[cls])
1262
return docutils.nodes.entry('', val, classes=[cls])
1264
def _pointer(self, ident):
1265
return docutils.nodes.paragraph('', '',
1266
docutils.nodes.inline(ident, ident,
1267
classes=['avm-pointer']))
1270
return docutils.nodes.paragraph('', '[]',
1271
classes=['avm-empty'])
1274
for key in self.keys:
1275
val = self.vals[key]
1276
key_node = self._entry(key, 'avm-key')
1277
if isinstance(val, AVMPointer):
1278
eq_node = self._entry(u'\u2192', 'avm-eq') # right arrow
1279
val_node = self._entry(self._pointer(val.ident), 'avm-val')
1280
elif isinstance(val, AVM):
1281
eq_node = self._entry('=', 'avm-eq')
1282
val_node = self._entry(val.as_table(), 'avm-val')
1284
value = ('%s' % val.val).replace(' ', u'\u00a0') # =nbsp
1285
eq_node = self._entry('=', 'avm-eq')
1286
val_node = self._entry(value, 'avm-val')
1288
rows.append(docutils.nodes.row('', key_node, eq_node, val_node))
1290
# Add left/right bracket nodes:
1291
if len(self.keys)==1: vpos = 'topbot'
1292
elif key == self.keys[0]: vpos = 'top'
1293
elif key == self.keys[-1]: vpos = 'bot'
1295
rows[-1].insert(0, self._entry(u'\u00a0', 'avm-%sleft' % vpos))
1296
rows[-1].append(self._entry(u'\u00a0', 'avm-%sright' % vpos))
1299
if key == self.keys[0] and self.ident:
1300
rows[-1].append(self._entry(self._pointer(self.ident),
1303
rows[-1].append(self._entry(u'\u00a0', 'avm-ident'))
1305
colspecs = [docutils.nodes.colspec(colwidth=1) for i in range(6)]
1307
tbody = docutils.nodes.tbody('', *rows)
1308
tgroup = docutils.nodes.tgroup('', cols=3, *(colspecs+[tbody]))
1309
table = docutils.nodes.table('', tgroup, classes=['avm'])
1313
def __init__(self, ident, val):
1317
if self.ident: return '%s[%s]' % (self.val, self.ident)
1318
else: return '%r' % self.val
1319
def _as_latex(self, indent=0):
1320
return '%s' % self.val
1323
def __init__(self, ident):
1326
return '[%s]' % self.ident
1327
def _as_latex(self, indent=0):
1328
return '\\@{%s}' % self.ident
1330
def parse_avm(s, ident=None):
1331
lines = [l.rstrip() for l in s.split('\n') if l.strip()]
1332
if not lines: raise ValueError(0)
1333
lines.append('[%s]' % (' '*(len(lines[0])-2)))
1335
# Create our new AVM.
1338
w = len(lines[0]) # Line width
1339
avmval_pos = None # (left, right, top) for nested AVMs
1340
key = None # Key for nested AVMs
1341
ident = None # Identifier for nested AVMs
1343
NESTED = re.compile(r'\[\s+(\[.*\])\s*\]$')
1344
ASSIGN = re.compile(r'\[\s*(?P<KEY>[^\[=>]+?)\s*'
1346
r'(\((?P<ID>\d+)\))?\s*'
1347
r'((?P<VAL>.+?))\s*\]$')
1348
BLANK = re.compile(r'\[\s+\]$')
1350
for lineno, line in enumerate(lines):
1351
#debug('%s %s %s %r' % (lineno, key, avmval_pos, line))
1352
if line[0] != '[' or line[-1] != ']' or len(line) != w:
1353
raise ValueError(lineno)
1355
nested_m = NESTED.match(line)
1356
assign_m = ASSIGN.match(line)
1357
blank_m = BLANK.match(line)
1358
if not (nested_m or assign_m or blank_m):
1359
raise ValueError(lineno)
1361
if nested_m or (assign_m and assign_m.group('VAL').startswith('[')):
1362
left, right = line.index('[',1), line.rindex(']', 0, -1)+1
1363
if avmval_pos is None:
1364
avmval_pos = (left, right, lineno)
1365
elif avmval_pos[:2] != (left, right):
1366
raise ValueError(lineno)
1369
if assign_m.group('VAL').startswith('['):
1370
if key is not None: raise ValueError(lineno)
1371
if assign_m.group('EQ') != '=': raise ValueError(lineno)
1372
key = assign_m.group('KEY')
1373
ident = assign_m.group('ID')
1375
if assign_m.group('EQ') == '=':
1376
avm.assign(assign_m.group('KEY'),
1377
AVMValue(assign_m.group('ID'),
1378
assign_m.group('VAL')))
1380
if assign_m.group('VAL').strip(): raise ValueError(lineno)
1381
avm.assign(assign_m.group('KEY'),
1382
AVMPointer(assign_m.group('ID')))
1384
if blank_m and avmval_pos is not None:
1385
left, right, top = avmval_pos
1386
valstr = '\n'.join(l[left:right] for l in lines[top:lineno])
1387
avm.assign(key, parse_avm(valstr, ident))
1388
key = avmval_pos = None
44
1394
######################################################################
45
1395
#{ Doctest Indentation
46
1396
######################################################################