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