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