##// END OF EJS Templates
minirst: do not add a 2nd empty paragraph...
Simon Heimberg -
r19995:0f6e360b stable
parent child Browse files
Show More
@@ -1,703 +1,706 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, t)
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(dict(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 onw 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(dict(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 264 prune = containertype not in keep
265 265 if prune:
266 266 pruned.append(containertype)
267 267
268 268 # Always delete "..container:: type" block
269 269 del blocks[i]
270 270 j = i
271 271 i -= 1
272 272 while j < len(blocks) and blocks[j]['indent'] > indent:
273 273 if prune:
274 274 del blocks[j]
275 275 else:
276 276 blocks[j]['indent'] -= adjustment
277 277 j += 1
278 278 i += 1
279 279 return blocks, pruned
280 280
281 281 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
282 282
283 283 def findtables(blocks):
284 284 '''Find simple tables
285 285
286 286 Only simple one-line table elements are supported
287 287 '''
288 288
289 289 for block in blocks:
290 290 # Searching for a block that looks like this:
291 291 #
292 292 # === ==== ===
293 293 # A B C
294 294 # === ==== === <- optional
295 295 # 1 2 3
296 296 # x y z
297 297 # === ==== ===
298 298 if (block['type'] == 'paragraph' and
299 299 len(block['lines']) > 2 and
300 300 _tablere.match(block['lines'][0]) and
301 301 block['lines'][0] == block['lines'][-1]):
302 302 block['type'] = 'table'
303 303 block['header'] = False
304 304 div = block['lines'][0]
305 305
306 306 # column markers are ASCII so we can calculate column
307 307 # position in bytes
308 308 columns = [x for x in xrange(len(div))
309 309 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
310 310 rows = []
311 311 for l in block['lines'][1:-1]:
312 312 if l == div:
313 313 block['header'] = True
314 314 continue
315 315 row = []
316 316 # we measure columns not in bytes or characters but in
317 317 # colwidth which makes things tricky
318 318 pos = columns[0] # leading whitespace is bytes
319 319 for n, start in enumerate(columns):
320 320 if n + 1 < len(columns):
321 321 width = columns[n + 1] - start
322 322 v = encoding.getcols(l, pos, width) # gather columns
323 323 pos += len(v) # calculate byte position of end
324 324 row.append(v.strip())
325 325 else:
326 326 row.append(l[pos:].strip())
327 327 rows.append(row)
328 328
329 329 block['table'] = rows
330 330
331 331 return blocks
332 332
333 333 def findsections(blocks):
334 334 """Finds sections.
335 335
336 336 The blocks must have a 'type' field, i.e., they should have been
337 337 run through findliteralblocks first.
338 338 """
339 339 for block in blocks:
340 340 # Searching for a block that looks like this:
341 341 #
342 342 # +------------------------------+
343 343 # | Section title |
344 344 # | ------------- |
345 345 # +------------------------------+
346 346 if (block['type'] == 'paragraph' and
347 347 len(block['lines']) == 2 and
348 348 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
349 349 _sectionre.match(block['lines'][1])):
350 350 block['underline'] = block['lines'][1][0]
351 351 block['type'] = 'section'
352 352 del block['lines'][1]
353 353 return blocks
354 354
355 355 def inlineliterals(blocks):
356 356 substs = [('``', '"')]
357 357 for b in blocks:
358 358 if b['type'] in ('paragraph', 'section'):
359 359 b['lines'] = [replace(l, substs) for l in b['lines']]
360 360 return blocks
361 361
362 362 def hgrole(blocks):
363 363 substs = [(':hg:`', '"hg '), ('`', '"')]
364 364 for b in blocks:
365 365 if b['type'] in ('paragraph', 'section'):
366 366 # Turn :hg:`command` into "hg command". This also works
367 367 # when there is a line break in the command and relies on
368 368 # the fact that we have no stray back-quotes in the input
369 369 # (run the blocks through inlineliterals first).
370 370 b['lines'] = [replace(l, substs) for l in b['lines']]
371 371 return blocks
372 372
373 373 def addmargins(blocks):
374 374 """Adds empty blocks for vertical spacing.
375 375
376 376 This groups bullets, options, and definitions together with no vertical
377 377 space between them, and adds an empty block between all other blocks.
378 378 """
379 379 i = 1
380 380 while i < len(blocks):
381 381 if (blocks[i]['type'] == blocks[i - 1]['type'] and
382 382 blocks[i]['type'] in ('bullet', 'option', 'field')):
383 383 i += 1
384 elif not blocks[i - 1]['lines']:
385 # no lines in previous block, do not seperate
386 i += 1
384 387 else:
385 388 blocks.insert(i, dict(lines=[''], indent=0, type='margin'))
386 389 i += 2
387 390 return blocks
388 391
389 392 def prunecomments(blocks):
390 393 """Remove comments."""
391 394 i = 0
392 395 while i < len(blocks):
393 396 b = blocks[i]
394 397 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
395 398 b['lines'] == ['..']):
396 399 del blocks[i]
397 400 if i < len(blocks) and blocks[i]['type'] == 'margin':
398 401 del blocks[i]
399 402 else:
400 403 i += 1
401 404 return blocks
402 405
403 406 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
404 407 r"error|hint|important|note|tip|warning)::",
405 408 flags=re.IGNORECASE)
406 409
407 410 def findadmonitions(blocks):
408 411 """
409 412 Makes the type of the block an admonition block if
410 413 the first line is an admonition directive
411 414 """
412 415 i = 0
413 416 while i < len(blocks):
414 417 m = _admonitionre.match(blocks[i]['lines'][0])
415 418 if m:
416 419 blocks[i]['type'] = 'admonition'
417 420 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
418 421
419 422 firstline = blocks[i]['lines'][0][m.end() + 1:]
420 423 if firstline:
421 424 blocks[i]['lines'].insert(1, ' ' + firstline)
422 425
423 426 blocks[i]['admonitiontitle'] = admonitiontitle
424 427 del blocks[i]['lines'][0]
425 428 i = i + 1
426 429 return blocks
427 430
428 431 _admonitiontitles = {'attention': _('Attention:'),
429 432 'caution': _('Caution:'),
430 433 'danger': _('!Danger!') ,
431 434 'error': _('Error:'),
432 435 'hint': _('Hint:'),
433 436 'important': _('Important:'),
434 437 'note': _('Note:'),
435 438 'tip': _('Tip:'),
436 439 'warning': _('Warning!')}
437 440
438 441 def formatoption(block, width):
439 442 desc = ' '.join(map(str.strip, block['lines']))
440 443 colwidth = encoding.colwidth(block['optstr'])
441 444 usablewidth = width - 1
442 445 hanging = block['optstrwidth']
443 446 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
444 447 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
445 448 return ' %s\n' % (util.wrap(desc, usablewidth,
446 449 initindent=initindent,
447 450 hangindent=hangindent))
448 451
449 452 def formatblock(block, width):
450 453 """Format a block according to width."""
451 454 if width <= 0:
452 455 width = 78
453 456 indent = ' ' * block['indent']
454 457 if block['type'] == 'admonition':
455 458 admonition = _admonitiontitles[block['admonitiontitle']]
456 459 if not block['lines']:
457 460 return indent + admonition + '\n'
458 461 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
459 462
460 463 defindent = indent + hang * ' '
461 464 text = ' '.join(map(str.strip, block['lines']))
462 465 return '%s\n%s\n' % (indent + admonition,
463 466 util.wrap(text, width=width,
464 467 initindent=defindent,
465 468 hangindent=defindent))
466 469 if block['type'] == 'margin':
467 470 return '\n'
468 471 if block['type'] == 'literal':
469 472 indent += ' '
470 473 return indent + ('\n' + indent).join(block['lines']) + '\n'
471 474 if block['type'] == 'section':
472 475 underline = encoding.colwidth(block['lines'][0]) * block['underline']
473 476 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
474 477 if block['type'] == 'table':
475 478 table = block['table']
476 479 # compute column widths
477 480 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
478 481 text = ''
479 482 span = sum(widths) + len(widths) - 1
480 483 indent = ' ' * block['indent']
481 484 hang = ' ' * (len(indent) + span - widths[-1])
482 485
483 486 for row in table:
484 487 l = []
485 488 for w, v in zip(widths, row):
486 489 pad = ' ' * (w - encoding.colwidth(v))
487 490 l.append(v + pad)
488 491 l = ' '.join(l)
489 492 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
490 493 if not text and block['header']:
491 494 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
492 495 else:
493 496 text += l + "\n"
494 497 return text
495 498 if block['type'] == 'definition':
496 499 term = indent + block['lines'][0]
497 500 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
498 501 defindent = indent + hang * ' '
499 502 text = ' '.join(map(str.strip, block['lines'][1:]))
500 503 return '%s\n%s\n' % (term, util.wrap(text, width=width,
501 504 initindent=defindent,
502 505 hangindent=defindent))
503 506 subindent = indent
504 507 if block['type'] == 'bullet':
505 508 if block['lines'][0].startswith('| '):
506 509 # Remove bullet for line blocks and add no extra
507 510 # indention.
508 511 block['lines'][0] = block['lines'][0][2:]
509 512 else:
510 513 m = _bulletre.match(block['lines'][0])
511 514 subindent = indent + m.end() * ' '
512 515 elif block['type'] == 'field':
513 516 key = block['key']
514 517 subindent = indent + _fieldwidth * ' '
515 518 if len(key) + 2 > _fieldwidth:
516 519 # key too large, use full line width
517 520 key = key.ljust(width)
518 521 else:
519 522 # key fits within field width
520 523 key = key.ljust(_fieldwidth)
521 524 block['lines'][0] = key + block['lines'][0]
522 525 elif block['type'] == 'option':
523 526 return formatoption(block, width)
524 527
525 528 text = ' '.join(map(str.strip, block['lines']))
526 529 return util.wrap(text, width=width,
527 530 initindent=indent,
528 531 hangindent=subindent) + '\n'
529 532
530 533 def formathtml(blocks):
531 534 """Format RST blocks as HTML"""
532 535
533 536 out = []
534 537 headernest = ''
535 538 listnest = []
536 539
537 540 def escape(s):
538 541 return cgi.escape(s, True)
539 542
540 543 def openlist(start, level):
541 544 if not listnest or listnest[-1][0] != start:
542 545 listnest.append((start, level))
543 546 out.append('<%s>\n' % start)
544 547
545 548 blocks = [b for b in blocks if b['type'] != 'margin']
546 549
547 550 for pos, b in enumerate(blocks):
548 551 btype = b['type']
549 552 level = b['indent']
550 553 lines = b['lines']
551 554
552 555 if btype == 'admonition':
553 556 admonition = escape(_admonitiontitles[b['admonitiontitle']])
554 557 text = escape(' '.join(map(str.strip, lines)))
555 558 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
556 559 elif btype == 'paragraph':
557 560 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
558 561 elif btype == 'margin':
559 562 pass
560 563 elif btype == 'literal':
561 564 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
562 565 elif btype == 'section':
563 566 i = b['underline']
564 567 if i not in headernest:
565 568 headernest += i
566 569 level = headernest.index(i) + 1
567 570 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
568 571 elif btype == 'table':
569 572 table = b['table']
570 573 out.append('<table>\n')
571 574 for row in table:
572 575 out.append('<tr>')
573 576 for v in row:
574 577 out.append('<td>')
575 578 out.append(escape(v))
576 579 out.append('</td>')
577 580 out.append('\n')
578 581 out.pop()
579 582 out.append('</tr>\n')
580 583 out.append('</table>\n')
581 584 elif btype == 'definition':
582 585 openlist('dl', level)
583 586 term = escape(lines[0])
584 587 text = escape(' '.join(map(str.strip, lines[1:])))
585 588 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
586 589 elif btype == 'bullet':
587 590 bullet, head = lines[0].split(' ', 1)
588 591 if bullet == '-':
589 592 openlist('ul', level)
590 593 else:
591 594 openlist('ol', level)
592 595 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
593 596 elif btype == 'field':
594 597 openlist('dl', level)
595 598 key = escape(b['key'])
596 599 text = escape(' '.join(map(str.strip, lines)))
597 600 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
598 601 elif btype == 'option':
599 602 openlist('dl', level)
600 603 opt = escape(b['optstr'])
601 604 desc = escape(' '.join(map(str.strip, lines)))
602 605 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
603 606
604 607 # close lists if indent level of next block is lower
605 608 if listnest:
606 609 start, level = listnest[-1]
607 610 if pos == len(blocks) - 1:
608 611 out.append('</%s>\n' % start)
609 612 listnest.pop()
610 613 else:
611 614 nb = blocks[pos + 1]
612 615 ni = nb['indent']
613 616 if (ni < level or
614 617 (ni == level and
615 618 nb['type'] not in 'definition bullet field option')):
616 619 out.append('</%s>\n' % start)
617 620 listnest.pop()
618 621
619 622 return ''.join(out)
620 623
621 624 def parse(text, indent=0, keep=None):
622 625 """Parse text into a list of blocks"""
623 626 pruned = []
624 627 blocks = findblocks(text)
625 628 for b in blocks:
626 629 b['indent'] += indent
627 630 blocks = findliteralblocks(blocks)
628 631 blocks = findtables(blocks)
629 632 blocks, pruned = prunecontainers(blocks, keep or [])
630 633 blocks = findsections(blocks)
631 634 blocks = inlineliterals(blocks)
632 635 blocks = hgrole(blocks)
633 636 blocks = splitparagraphs(blocks)
634 637 blocks = updatefieldlists(blocks)
635 638 blocks = updateoptionlists(blocks)
636 639 blocks = findadmonitions(blocks)
637 640 blocks = addmargins(blocks)
638 641 blocks = prunecomments(blocks)
639 642 return blocks, pruned
640 643
641 644 def formatblocks(blocks, width):
642 645 text = ''.join(formatblock(b, width) for b in blocks)
643 646 return text
644 647
645 648 def format(text, width=80, indent=0, keep=None, style='plain'):
646 649 """Parse and format the text according to width."""
647 650 blocks, pruned = parse(text, indent, keep or [])
648 651 if style == 'html':
649 652 text = formathtml(blocks)
650 653 else:
651 654 text = ''.join(formatblock(b, width) for b in blocks)
652 655 if keep is None:
653 656 return text
654 657 else:
655 658 return text, pruned
656 659
657 660 def getsections(blocks):
658 661 '''return a list of (section name, nesting level, blocks) tuples'''
659 662 nest = ""
660 663 level = 0
661 664 secs = []
662 665 for b in blocks:
663 666 if b['type'] == 'section':
664 667 i = b['underline']
665 668 if i not in nest:
666 669 nest += i
667 670 level = nest.index(i) + 1
668 671 nest = nest[:level]
669 672 secs.append((b['lines'][0], level, [b]))
670 673 else:
671 674 if not secs:
672 675 # add an initial empty section
673 676 secs = [('', 0, [])]
674 677 secs[-1][2].append(b)
675 678 return secs
676 679
677 680 def decorateblocks(blocks, width):
678 681 '''generate a list of (section name, line text) pairs for search'''
679 682 lines = []
680 683 for s in getsections(blocks):
681 684 section = s[0]
682 685 text = formatblocks(s[2], width)
683 686 lines.append([(section, l) for l in text.splitlines(True)])
684 687 return lines
685 688
686 689 def maketable(data, indent=0, header=False):
687 690 '''Generate an RST table for the given table data as a list of lines'''
688 691
689 692 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
690 693 indent = ' ' * indent
691 694 div = indent + ' '.join('=' * w for w in widths) + '\n'
692 695
693 696 out = [div]
694 697 for row in data:
695 698 l = []
696 699 for w, v in zip(widths, row):
697 700 pad = ' ' * (w - encoding.colwidth(v))
698 701 l.append(v + pad)
699 702 out.append(indent + ' '.join(l) + "\n")
700 703 if header and len(data) > 1:
701 704 out.insert(2, div)
702 705 out.append(div)
703 706 return out
General Comments 0
You need to be logged in to leave comments. Login now