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