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