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