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