##// END OF EJS Templates
minirst: avoid shadowing a variable in a list comprehension
Augie Fackler -
r30390:bff77a69 default
parent child Browse files
Show More
@@ -1,816 +1,816 b''
1 1 # minirst.py - minimal reStructuredText parser
2 2 #
3 3 # Copyright 2009, 2010 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """simplified reStructuredText parser.
9 9
10 10 This parser knows just enough about reStructuredText to parse the
11 11 Mercurial docstrings.
12 12
13 13 It cheats in a major way: nested blocks are not really nested. They
14 14 are just indented blocks that look like they are nested. This relies
15 15 on the user to keep the right indentation for the blocks.
16 16
17 17 Remember to update https://mercurial-scm.org/wiki/HelpStyleGuide
18 18 when adding support for new constructs.
19 19 """
20 20
21 21 from __future__ import absolute_import
22 22
23 23 import cgi
24 24 import re
25 25
26 26 from .i18n import _
27 27 from . import (
28 28 encoding,
29 29 util,
30 30 )
31 31
32 32 def section(s):
33 33 return "%s\n%s\n\n" % (s, "\"" * encoding.colwidth(s))
34 34
35 35 def subsection(s):
36 36 return "%s\n%s\n\n" % (s, '=' * encoding.colwidth(s))
37 37
38 38 def subsubsection(s):
39 39 return "%s\n%s\n\n" % (s, "-" * encoding.colwidth(s))
40 40
41 41 def subsubsubsection(s):
42 42 return "%s\n%s\n\n" % (s, "." * encoding.colwidth(s))
43 43
44 44 def replace(text, substs):
45 45 '''
46 46 Apply a list of (find, replace) pairs to a text.
47 47
48 48 >>> replace("foo bar", [('f', 'F'), ('b', 'B')])
49 49 'Foo Bar'
50 50 >>> encoding.encoding = 'latin1'
51 51 >>> replace('\\x81\\\\', [('\\\\', '/')])
52 52 '\\x81/'
53 53 >>> encoding.encoding = 'shiftjis'
54 54 >>> replace('\\x81\\\\', [('\\\\', '/')])
55 55 '\\x81\\\\'
56 56 '''
57 57
58 58 # some character encodings (cp932 for Japanese, at least) use
59 59 # ASCII characters other than control/alphabet/digit as a part of
60 60 # multi-bytes characters, so direct replacing with such characters
61 61 # on strings in local encoding causes invalid byte sequences.
62 62 utext = text.decode(encoding.encoding)
63 63 for f, t in substs:
64 64 utext = utext.replace(f.decode("ascii"), t.decode("ascii"))
65 65 return utext.encode(encoding.encoding)
66 66
67 67 _blockre = re.compile(r"\n(?:\s*\n)+")
68 68
69 69 def findblocks(text):
70 70 """Find continuous blocks of lines in text.
71 71
72 72 Returns a list of dictionaries representing the blocks. Each block
73 73 has an 'indent' field and a 'lines' field.
74 74 """
75 75 blocks = []
76 76 for b in _blockre.split(text.lstrip('\n').rstrip()):
77 77 lines = b.splitlines()
78 78 if lines:
79 79 indent = min((len(l) - len(l.lstrip())) for l in lines)
80 80 lines = [l[indent:] for l in lines]
81 81 blocks.append({'indent': indent, 'lines': lines})
82 82 return blocks
83 83
84 84 def findliteralblocks(blocks):
85 85 """Finds literal blocks and adds a 'type' field to the blocks.
86 86
87 87 Literal blocks are given the type 'literal', all other blocks are
88 88 given type the 'paragraph'.
89 89 """
90 90 i = 0
91 91 while i < len(blocks):
92 92 # Searching for a block that looks like this:
93 93 #
94 94 # +------------------------------+
95 95 # | paragraph |
96 96 # | (ends with "::") |
97 97 # +------------------------------+
98 98 # +---------------------------+
99 99 # | indented literal block |
100 100 # +---------------------------+
101 101 blocks[i]['type'] = 'paragraph'
102 102 if blocks[i]['lines'][-1].endswith('::') and i + 1 < len(blocks):
103 103 indent = blocks[i]['indent']
104 104 adjustment = blocks[i + 1]['indent'] - indent
105 105
106 106 if blocks[i]['lines'] == ['::']:
107 107 # Expanded form: remove block
108 108 del blocks[i]
109 109 i -= 1
110 110 elif blocks[i]['lines'][-1].endswith(' ::'):
111 111 # Partially minimized form: remove space and both
112 112 # colons.
113 113 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-3]
114 114 elif len(blocks[i]['lines']) == 1 and \
115 115 blocks[i]['lines'][0].lstrip(' ').startswith('.. ') and \
116 116 blocks[i]['lines'][0].find(' ', 3) == -1:
117 117 # directive on its own line, not a literal block
118 118 i += 1
119 119 continue
120 120 else:
121 121 # Fully minimized form: remove just one colon.
122 122 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-1]
123 123
124 124 # List items are formatted with a hanging indent. We must
125 125 # correct for this here while we still have the original
126 126 # information on the indentation of the subsequent literal
127 127 # blocks available.
128 128 m = _bulletre.match(blocks[i]['lines'][0])
129 129 if m:
130 130 indent += m.end()
131 131 adjustment -= m.end()
132 132
133 133 # Mark the following indented blocks.
134 134 while i + 1 < len(blocks) and blocks[i + 1]['indent'] > indent:
135 135 blocks[i + 1]['type'] = 'literal'
136 136 blocks[i + 1]['indent'] -= adjustment
137 137 i += 1
138 138 i += 1
139 139 return blocks
140 140
141 141 _bulletre = re.compile(r'(-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ')
142 142 _optionre = re.compile(r'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)'
143 143 r'((.*) +)(.*)$')
144 144 _fieldre = re.compile(r':(?![: ])([^:]*)(?<! ):[ ]+(.*)')
145 145 _definitionre = re.compile(r'[^ ]')
146 146 _tablere = re.compile(r'(=+\s+)*=+')
147 147
148 148 def splitparagraphs(blocks):
149 149 """Split paragraphs into lists."""
150 150 # Tuples with (list type, item regexp, single line items?). Order
151 151 # matters: definition lists has the least specific regexp and must
152 152 # come last.
153 153 listtypes = [('bullet', _bulletre, True),
154 154 ('option', _optionre, True),
155 155 ('field', _fieldre, True),
156 156 ('definition', _definitionre, False)]
157 157
158 158 def match(lines, i, itemre, singleline):
159 159 """Does itemre match an item at line i?
160 160
161 161 A list item can be followed by an indented line or another list
162 162 item (but only if singleline is True).
163 163 """
164 164 line1 = lines[i]
165 165 line2 = i + 1 < len(lines) and lines[i + 1] or ''
166 166 if not itemre.match(line1):
167 167 return False
168 168 if singleline:
169 169 return line2 == '' or line2[0] == ' ' or itemre.match(line2)
170 170 else:
171 171 return line2.startswith(' ')
172 172
173 173 i = 0
174 174 while i < len(blocks):
175 175 if blocks[i]['type'] == 'paragraph':
176 176 lines = blocks[i]['lines']
177 177 for type, itemre, singleline in listtypes:
178 178 if match(lines, 0, itemre, singleline):
179 179 items = []
180 180 for j, line in enumerate(lines):
181 181 if match(lines, j, itemre, singleline):
182 182 items.append({'type': type, 'lines': [],
183 183 'indent': blocks[i]['indent']})
184 184 items[-1]['lines'].append(line)
185 185 blocks[i:i + 1] = items
186 186 break
187 187 i += 1
188 188 return blocks
189 189
190 190 _fieldwidth = 14
191 191
192 192 def updatefieldlists(blocks):
193 193 """Find key for field lists."""
194 194 i = 0
195 195 while i < len(blocks):
196 196 if blocks[i]['type'] != 'field':
197 197 i += 1
198 198 continue
199 199
200 200 j = i
201 201 while j < len(blocks) and blocks[j]['type'] == 'field':
202 202 m = _fieldre.match(blocks[j]['lines'][0])
203 203 key, rest = m.groups()
204 204 blocks[j]['lines'][0] = rest
205 205 blocks[j]['key'] = key
206 206 j += 1
207 207
208 208 i = j + 1
209 209
210 210 return blocks
211 211
212 212 def updateoptionlists(blocks):
213 213 i = 0
214 214 while i < len(blocks):
215 215 if blocks[i]['type'] != 'option':
216 216 i += 1
217 217 continue
218 218
219 219 optstrwidth = 0
220 220 j = i
221 221 while j < len(blocks) and blocks[j]['type'] == 'option':
222 222 m = _optionre.match(blocks[j]['lines'][0])
223 223
224 224 shortoption = m.group(2)
225 225 group3 = m.group(3)
226 226 longoption = group3[2:].strip()
227 227 desc = m.group(6).strip()
228 228 longoptionarg = m.group(5).strip()
229 229 blocks[j]['lines'][0] = desc
230 230
231 231 noshortop = ''
232 232 if not shortoption:
233 233 noshortop = ' '
234 234
235 235 opt = "%s%s" % (shortoption and "-%s " % shortoption or '',
236 236 ("%s--%s %s") % (noshortop, longoption,
237 237 longoptionarg))
238 238 opt = opt.rstrip()
239 239 blocks[j]['optstr'] = opt
240 240 optstrwidth = max(optstrwidth, encoding.colwidth(opt))
241 241 j += 1
242 242
243 243 for block in blocks[i:j]:
244 244 block['optstrwidth'] = optstrwidth
245 245 i = j + 1
246 246 return blocks
247 247
248 248 def prunecontainers(blocks, keep):
249 249 """Prune unwanted containers.
250 250
251 251 The blocks must have a 'type' field, i.e., they should have been
252 252 run through findliteralblocks first.
253 253 """
254 254 pruned = []
255 255 i = 0
256 256 while i + 1 < len(blocks):
257 257 # Searching for a block that looks like this:
258 258 #
259 259 # +-------+---------------------------+
260 260 # | ".. container ::" type |
261 261 # +---+ |
262 262 # | blocks |
263 263 # +-------------------------------+
264 264 if (blocks[i]['type'] == 'paragraph' and
265 265 blocks[i]['lines'][0].startswith('.. container::')):
266 266 indent = blocks[i]['indent']
267 267 adjustment = blocks[i + 1]['indent'] - indent
268 268 containertype = blocks[i]['lines'][0][15:]
269 269 prune = True
270 270 for c in keep:
271 271 if c in containertype.split('.'):
272 272 prune = False
273 273 if prune:
274 274 pruned.append(containertype)
275 275
276 276 # Always delete "..container:: type" block
277 277 del blocks[i]
278 278 j = i
279 279 i -= 1
280 280 while j < len(blocks) and blocks[j]['indent'] > indent:
281 281 if prune:
282 282 del blocks[j]
283 283 else:
284 284 blocks[j]['indent'] -= adjustment
285 285 j += 1
286 286 i += 1
287 287 return blocks, pruned
288 288
289 289 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
290 290
291 291 def findtables(blocks):
292 292 '''Find simple tables
293 293
294 294 Only simple one-line table elements are supported
295 295 '''
296 296
297 297 for block in blocks:
298 298 # Searching for a block that looks like this:
299 299 #
300 300 # === ==== ===
301 301 # A B C
302 302 # === ==== === <- optional
303 303 # 1 2 3
304 304 # x y z
305 305 # === ==== ===
306 306 if (block['type'] == 'paragraph' and
307 307 len(block['lines']) > 2 and
308 308 _tablere.match(block['lines'][0]) and
309 309 block['lines'][0] == block['lines'][-1]):
310 310 block['type'] = 'table'
311 311 block['header'] = False
312 312 div = block['lines'][0]
313 313
314 314 # column markers are ASCII so we can calculate column
315 315 # position in bytes
316 316 columns = [x for x in xrange(len(div))
317 317 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
318 318 rows = []
319 319 for l in block['lines'][1:-1]:
320 320 if l == div:
321 321 block['header'] = True
322 322 continue
323 323 row = []
324 324 # we measure columns not in bytes or characters but in
325 325 # colwidth which makes things tricky
326 326 pos = columns[0] # leading whitespace is bytes
327 327 for n, start in enumerate(columns):
328 328 if n + 1 < len(columns):
329 329 width = columns[n + 1] - start
330 330 v = encoding.getcols(l, pos, width) # gather columns
331 331 pos += len(v) # calculate byte position of end
332 332 row.append(v.strip())
333 333 else:
334 334 row.append(l[pos:].strip())
335 335 rows.append(row)
336 336
337 337 block['table'] = rows
338 338
339 339 return blocks
340 340
341 341 def findsections(blocks):
342 342 """Finds sections.
343 343
344 344 The blocks must have a 'type' field, i.e., they should have been
345 345 run through findliteralblocks first.
346 346 """
347 347 for block in blocks:
348 348 # Searching for a block that looks like this:
349 349 #
350 350 # +------------------------------+
351 351 # | Section title |
352 352 # | ------------- |
353 353 # +------------------------------+
354 354 if (block['type'] == 'paragraph' and
355 355 len(block['lines']) == 2 and
356 356 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
357 357 _sectionre.match(block['lines'][1])):
358 358 block['underline'] = block['lines'][1][0]
359 359 block['type'] = 'section'
360 360 del block['lines'][1]
361 361 return blocks
362 362
363 363 def inlineliterals(blocks):
364 364 substs = [('``', '"')]
365 365 for b in blocks:
366 366 if b['type'] in ('paragraph', 'section'):
367 367 b['lines'] = [replace(l, substs) for l in b['lines']]
368 368 return blocks
369 369
370 370 def hgrole(blocks):
371 371 substs = [(':hg:`', "'hg "), ('`', "'")]
372 372 for b in blocks:
373 373 if b['type'] in ('paragraph', 'section'):
374 374 # Turn :hg:`command` into "hg command". This also works
375 375 # when there is a line break in the command and relies on
376 376 # the fact that we have no stray back-quotes in the input
377 377 # (run the blocks through inlineliterals first).
378 378 b['lines'] = [replace(l, substs) for l in b['lines']]
379 379 return blocks
380 380
381 381 def addmargins(blocks):
382 382 """Adds empty blocks for vertical spacing.
383 383
384 384 This groups bullets, options, and definitions together with no vertical
385 385 space between them, and adds an empty block between all other blocks.
386 386 """
387 387 i = 1
388 388 while i < len(blocks):
389 389 if (blocks[i]['type'] == blocks[i - 1]['type'] and
390 390 blocks[i]['type'] in ('bullet', 'option', 'field')):
391 391 i += 1
392 392 elif not blocks[i - 1]['lines']:
393 393 # no lines in previous block, do not separate
394 394 i += 1
395 395 else:
396 396 blocks.insert(i, {'lines': [''], 'indent': 0, 'type': 'margin'})
397 397 i += 2
398 398 return blocks
399 399
400 400 def prunecomments(blocks):
401 401 """Remove comments."""
402 402 i = 0
403 403 while i < len(blocks):
404 404 b = blocks[i]
405 405 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
406 406 b['lines'] == ['..']):
407 407 del blocks[i]
408 408 if i < len(blocks) and blocks[i]['type'] == 'margin':
409 409 del blocks[i]
410 410 else:
411 411 i += 1
412 412 return blocks
413 413
414 414 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
415 415 r"error|hint|important|note|tip|warning)::",
416 416 flags=re.IGNORECASE)
417 417
418 418 def findadmonitions(blocks):
419 419 """
420 420 Makes the type of the block an admonition block if
421 421 the first line is an admonition directive
422 422 """
423 423 i = 0
424 424 while i < len(blocks):
425 425 m = _admonitionre.match(blocks[i]['lines'][0])
426 426 if m:
427 427 blocks[i]['type'] = 'admonition'
428 428 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
429 429
430 430 firstline = blocks[i]['lines'][0][m.end() + 1:]
431 431 if firstline:
432 432 blocks[i]['lines'].insert(1, ' ' + firstline)
433 433
434 434 blocks[i]['admonitiontitle'] = admonitiontitle
435 435 del blocks[i]['lines'][0]
436 436 i = i + 1
437 437 return blocks
438 438
439 439 _admonitiontitles = {'attention': _('Attention:'),
440 440 'caution': _('Caution:'),
441 441 'danger': _('!Danger!') ,
442 442 'error': _('Error:'),
443 443 'hint': _('Hint:'),
444 444 'important': _('Important:'),
445 445 'note': _('Note:'),
446 446 'tip': _('Tip:'),
447 447 'warning': _('Warning!')}
448 448
449 449 def formatoption(block, width):
450 450 desc = ' '.join(map(str.strip, block['lines']))
451 451 colwidth = encoding.colwidth(block['optstr'])
452 452 usablewidth = width - 1
453 453 hanging = block['optstrwidth']
454 454 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
455 455 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
456 456 return ' %s\n' % (util.wrap(desc, usablewidth,
457 457 initindent=initindent,
458 458 hangindent=hangindent))
459 459
460 460 def formatblock(block, width):
461 461 """Format a block according to width."""
462 462 if width <= 0:
463 463 width = 78
464 464 indent = ' ' * block['indent']
465 465 if block['type'] == 'admonition':
466 466 admonition = _admonitiontitles[block['admonitiontitle']]
467 467 if not block['lines']:
468 468 return indent + admonition + '\n'
469 469 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
470 470
471 471 defindent = indent + hang * ' '
472 472 text = ' '.join(map(str.strip, block['lines']))
473 473 return '%s\n%s\n' % (indent + admonition,
474 474 util.wrap(text, width=width,
475 475 initindent=defindent,
476 476 hangindent=defindent))
477 477 if block['type'] == 'margin':
478 478 return '\n'
479 479 if block['type'] == 'literal':
480 480 indent += ' '
481 481 return indent + ('\n' + indent).join(block['lines']) + '\n'
482 482 if block['type'] == 'section':
483 483 underline = encoding.colwidth(block['lines'][0]) * block['underline']
484 484 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
485 485 if block['type'] == 'table':
486 486 table = block['table']
487 487 # compute column widths
488 488 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
489 489 text = ''
490 490 span = sum(widths) + len(widths) - 1
491 491 indent = ' ' * block['indent']
492 492 hang = ' ' * (len(indent) + span - widths[-1])
493 493
494 494 for row in table:
495 495 l = []
496 496 for w, v in zip(widths, row):
497 497 pad = ' ' * (w - encoding.colwidth(v))
498 498 l.append(v + pad)
499 499 l = ' '.join(l)
500 500 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
501 501 if not text and block['header']:
502 502 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
503 503 else:
504 504 text += l + "\n"
505 505 return text
506 506 if block['type'] == 'definition':
507 507 term = indent + block['lines'][0]
508 508 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
509 509 defindent = indent + hang * ' '
510 510 text = ' '.join(map(str.strip, block['lines'][1:]))
511 511 return '%s\n%s\n' % (term, util.wrap(text, width=width,
512 512 initindent=defindent,
513 513 hangindent=defindent))
514 514 subindent = indent
515 515 if block['type'] == 'bullet':
516 516 if block['lines'][0].startswith('| '):
517 517 # Remove bullet for line blocks and add no extra
518 518 # indentation.
519 519 block['lines'][0] = block['lines'][0][2:]
520 520 else:
521 521 m = _bulletre.match(block['lines'][0])
522 522 subindent = indent + m.end() * ' '
523 523 elif block['type'] == 'field':
524 524 key = block['key']
525 525 subindent = indent + _fieldwidth * ' '
526 526 if len(key) + 2 > _fieldwidth:
527 527 # key too large, use full line width
528 528 key = key.ljust(width)
529 529 else:
530 530 # key fits within field width
531 531 key = key.ljust(_fieldwidth)
532 532 block['lines'][0] = key + block['lines'][0]
533 533 elif block['type'] == 'option':
534 534 return formatoption(block, width)
535 535
536 536 text = ' '.join(map(str.strip, block['lines']))
537 537 return util.wrap(text, width=width,
538 538 initindent=indent,
539 539 hangindent=subindent) + '\n'
540 540
541 541 def formathtml(blocks):
542 542 """Format RST blocks as HTML"""
543 543
544 544 out = []
545 545 headernest = ''
546 546 listnest = []
547 547
548 548 def escape(s):
549 549 return cgi.escape(s, True)
550 550
551 551 def openlist(start, level):
552 552 if not listnest or listnest[-1][0] != start:
553 553 listnest.append((start, level))
554 554 out.append('<%s>\n' % start)
555 555
556 556 blocks = [b for b in blocks if b['type'] != 'margin']
557 557
558 558 for pos, b in enumerate(blocks):
559 559 btype = b['type']
560 560 level = b['indent']
561 561 lines = b['lines']
562 562
563 563 if btype == 'admonition':
564 564 admonition = escape(_admonitiontitles[b['admonitiontitle']])
565 565 text = escape(' '.join(map(str.strip, lines)))
566 566 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
567 567 elif btype == 'paragraph':
568 568 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
569 569 elif btype == 'margin':
570 570 pass
571 571 elif btype == 'literal':
572 572 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
573 573 elif btype == 'section':
574 574 i = b['underline']
575 575 if i not in headernest:
576 576 headernest += i
577 577 level = headernest.index(i) + 1
578 578 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
579 579 elif btype == 'table':
580 580 table = b['table']
581 581 out.append('<table>\n')
582 582 for row in table:
583 583 out.append('<tr>')
584 584 for v in row:
585 585 out.append('<td>')
586 586 out.append(escape(v))
587 587 out.append('</td>')
588 588 out.append('\n')
589 589 out.pop()
590 590 out.append('</tr>\n')
591 591 out.append('</table>\n')
592 592 elif btype == 'definition':
593 593 openlist('dl', level)
594 594 term = escape(lines[0])
595 595 text = escape(' '.join(map(str.strip, lines[1:])))
596 596 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
597 597 elif btype == 'bullet':
598 598 bullet, head = lines[0].split(' ', 1)
599 599 if bullet == '-':
600 600 openlist('ul', level)
601 601 else:
602 602 openlist('ol', level)
603 603 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
604 604 elif btype == 'field':
605 605 openlist('dl', level)
606 606 key = escape(b['key'])
607 607 text = escape(' '.join(map(str.strip, lines)))
608 608 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
609 609 elif btype == 'option':
610 610 openlist('dl', level)
611 611 opt = escape(b['optstr'])
612 612 desc = escape(' '.join(map(str.strip, lines)))
613 613 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
614 614
615 615 # close lists if indent level of next block is lower
616 616 if listnest:
617 617 start, level = listnest[-1]
618 618 if pos == len(blocks) - 1:
619 619 out.append('</%s>\n' % start)
620 620 listnest.pop()
621 621 else:
622 622 nb = blocks[pos + 1]
623 623 ni = nb['indent']
624 624 if (ni < level or
625 625 (ni == level and
626 626 nb['type'] not in 'definition bullet field option')):
627 627 out.append('</%s>\n' % start)
628 628 listnest.pop()
629 629
630 630 return ''.join(out)
631 631
632 632 def parse(text, indent=0, keep=None):
633 633 """Parse text into a list of blocks"""
634 634 pruned = []
635 635 blocks = findblocks(text)
636 636 for b in blocks:
637 637 b['indent'] += indent
638 638 blocks = findliteralblocks(blocks)
639 639 blocks = findtables(blocks)
640 640 blocks, pruned = prunecontainers(blocks, keep or [])
641 641 blocks = findsections(blocks)
642 642 blocks = inlineliterals(blocks)
643 643 blocks = hgrole(blocks)
644 644 blocks = splitparagraphs(blocks)
645 645 blocks = updatefieldlists(blocks)
646 646 blocks = updateoptionlists(blocks)
647 647 blocks = findadmonitions(blocks)
648 648 blocks = addmargins(blocks)
649 649 blocks = prunecomments(blocks)
650 650 return blocks, pruned
651 651
652 652 def formatblocks(blocks, width):
653 653 text = ''.join(formatblock(b, width) for b in blocks)
654 654 return text
655 655
656 656 def format(text, width=80, indent=0, keep=None, style='plain', section=None):
657 657 """Parse and format the text according to width."""
658 658 blocks, pruned = parse(text, indent, keep or [])
659 659 parents = []
660 660 if section:
661 661 sections = getsections(blocks)
662 662 blocks = []
663 663 i = 0
664 664 lastparents = []
665 665 synthetic = []
666 666 collapse = True
667 667 while i < len(sections):
668 668 name, nest, b = sections[i]
669 669 del parents[nest:]
670 670 parents.append(i)
671 671 if name == section:
672 672 if lastparents != parents:
673 673 llen = len(lastparents)
674 674 plen = len(parents)
675 675 if llen and llen != plen:
676 676 collapse = False
677 677 s = []
678 678 for j in xrange(3, plen - 1):
679 679 parent = parents[j]
680 680 if (j >= llen or
681 681 lastparents[j] != parent):
682 682 s.append(len(blocks))
683 683 sec = sections[parent][2]
684 684 blocks.append(sec[0])
685 685 blocks.append(sec[-1])
686 686 if s:
687 687 synthetic.append(s)
688 688
689 689 lastparents = parents[:]
690 690 blocks.extend(b)
691 691
692 692 ## Also show all subnested sections
693 693 while i + 1 < len(sections) and sections[i + 1][1] > nest:
694 694 i += 1
695 695 blocks.extend(sections[i][2])
696 696 i += 1
697 697 if collapse:
698 698 synthetic.reverse()
699 699 for s in synthetic:
700 path = [blocks[i]['lines'][0] for i in s]
700 path = [blocks[syn]['lines'][0] for syn in s]
701 701 real = s[-1] + 2
702 702 realline = blocks[real]['lines']
703 703 realline[0] = ('"%s"' %
704 704 '.'.join(path + [realline[0]]).replace('"', ''))
705 705 del blocks[s[0]:real]
706 706
707 707 if style == 'html':
708 708 text = formathtml(blocks)
709 709 else:
710 710 text = ''.join(formatblock(b, width) for b in blocks)
711 711 if keep is None:
712 712 return text
713 713 else:
714 714 return text, pruned
715 715
716 716 def getsections(blocks):
717 717 '''return a list of (section name, nesting level, blocks) tuples'''
718 718 nest = ""
719 719 level = 0
720 720 secs = []
721 721
722 722 def getname(b):
723 723 if b['type'] == 'field':
724 724 x = b['key']
725 725 else:
726 726 x = b['lines'][0]
727 727 x = encoding.lower(x).strip('"')
728 728 if '(' in x:
729 729 x = x.split('(')[0]
730 730 return x
731 731
732 732 for b in blocks:
733 733 if b['type'] == 'section':
734 734 i = b['underline']
735 735 if i not in nest:
736 736 nest += i
737 737 level = nest.index(i) + 1
738 738 nest = nest[:level]
739 739 secs.append((getname(b), level, [b]))
740 740 elif b['type'] in ('definition', 'field'):
741 741 i = ' '
742 742 if i not in nest:
743 743 nest += i
744 744 level = nest.index(i) + 1
745 745 nest = nest[:level]
746 746 for i in range(1, len(secs) + 1):
747 747 sec = secs[-i]
748 748 if sec[1] < level:
749 749 break
750 750 siblings = [a for a in sec[2] if a['type'] == 'definition']
751 751 if siblings:
752 752 siblingindent = siblings[-1]['indent']
753 753 indent = b['indent']
754 754 if siblingindent < indent:
755 755 level += 1
756 756 break
757 757 elif siblingindent == indent:
758 758 level = sec[1]
759 759 break
760 760 secs.append((getname(b), level, [b]))
761 761 else:
762 762 if not secs:
763 763 # add an initial empty section
764 764 secs = [('', 0, [])]
765 765 if b['type'] != 'margin':
766 766 pointer = 1
767 767 bindent = b['indent']
768 768 while pointer < len(secs):
769 769 section = secs[-pointer][2][0]
770 770 if section['type'] != 'margin':
771 771 sindent = section['indent']
772 772 if len(section['lines']) > 1:
773 773 sindent += len(section['lines'][1]) - \
774 774 len(section['lines'][1].lstrip(' '))
775 775 if bindent >= sindent:
776 776 break
777 777 pointer += 1
778 778 if pointer > 1:
779 779 blevel = secs[-pointer][1]
780 780 if section['type'] != b['type']:
781 781 blevel += 1
782 782 secs.append(('', blevel, []))
783 783 secs[-1][2].append(b)
784 784 return secs
785 785
786 786 def decorateblocks(blocks, width):
787 787 '''generate a list of (section name, line text) pairs for search'''
788 788 lines = []
789 789 for s in getsections(blocks):
790 790 section = s[0]
791 791 text = formatblocks(s[2], width)
792 792 lines.append([(section, l) for l in text.splitlines(True)])
793 793 return lines
794 794
795 795 def maketable(data, indent=0, header=False):
796 796 '''Generate an RST table for the given table data as a list of lines'''
797 797
798 798 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
799 799 indent = ' ' * indent
800 800 div = indent + ' '.join('=' * w for w in widths) + '\n'
801 801
802 802 out = [div]
803 803 for row in data:
804 804 l = []
805 805 for w, v in zip(widths, row):
806 806 if '\n' in v:
807 807 # only remove line breaks and indentation, long lines are
808 808 # handled by the next tool
809 809 v = ' '.join(e.lstrip() for e in v.split('\n'))
810 810 pad = ' ' * (w - encoding.colwidth(v))
811 811 l.append(v + pad)
812 812 out.append(indent + ' '.join(l) + "\n")
813 813 if header and len(data) > 1:
814 814 out.insert(2, div)
815 815 out.append(div)
816 816 return out
General Comments 0
You need to be logged in to leave comments. Login now