##// END OF EJS Templates
minirst: reindent _admonitiontitles...
Gregory Szorc -
r31712:b3640334 default
parent child Browse files
Show More
@@ -1,832 +1,834 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 _admonitiontitles = {'attention': _('Attention:'),
456 'caution': _('Caution:'),
457 'danger': _('!Danger!') ,
458 'error': _('Error:'),
459 'hint': _('Hint:'),
460 'important': _('Important:'),
461 'note': _('Note:'),
462 'tip': _('Tip:'),
463 'warning': _('Warning!')}
455 _admonitiontitles = {
456 'attention': _('Attention:'),
457 'caution': _('Caution:'),
458 'danger': _('!Danger!'),
459 'error': _('Error:'),
460 'hint': _('Hint:'),
461 'important': _('Important:'),
462 'note': _('Note:'),
463 'tip': _('Tip:'),
464 'warning': _('Warning!'),
465 }
464 466
465 467 def formatoption(block, width):
466 468 desc = ' '.join(map(str.strip, block['lines']))
467 469 colwidth = encoding.colwidth(block['optstr'])
468 470 usablewidth = width - 1
469 471 hanging = block['optstrwidth']
470 472 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
471 473 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
472 474 return ' %s\n' % (util.wrap(desc, usablewidth,
473 475 initindent=initindent,
474 476 hangindent=hangindent))
475 477
476 478 def formatblock(block, width):
477 479 """Format a block according to width."""
478 480 if width <= 0:
479 481 width = 78
480 482 indent = ' ' * block['indent']
481 483 if block['type'] == 'admonition':
482 484 admonition = _admonitiontitles[block['admonitiontitle']]
483 485 if not block['lines']:
484 486 return indent + admonition + '\n'
485 487 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
486 488
487 489 defindent = indent + hang * ' '
488 490 text = ' '.join(map(str.strip, block['lines']))
489 491 return '%s\n%s\n' % (indent + admonition,
490 492 util.wrap(text, width=width,
491 493 initindent=defindent,
492 494 hangindent=defindent))
493 495 if block['type'] == 'margin':
494 496 return '\n'
495 497 if block['type'] == 'literal':
496 498 indent += ' '
497 499 return indent + ('\n' + indent).join(block['lines']) + '\n'
498 500 if block['type'] == 'section':
499 501 underline = encoding.colwidth(block['lines'][0]) * block['underline']
500 502 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
501 503 if block['type'] == 'table':
502 504 table = block['table']
503 505 # compute column widths
504 506 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
505 507 text = ''
506 508 span = sum(widths) + len(widths) - 1
507 509 indent = ' ' * block['indent']
508 510 hang = ' ' * (len(indent) + span - widths[-1])
509 511
510 512 for row in table:
511 513 l = []
512 514 for w, v in zip(widths, row):
513 515 pad = ' ' * (w - encoding.colwidth(v))
514 516 l.append(v + pad)
515 517 l = ' '.join(l)
516 518 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
517 519 if not text and block['header']:
518 520 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
519 521 else:
520 522 text += l + "\n"
521 523 return text
522 524 if block['type'] == 'definition':
523 525 term = indent + block['lines'][0]
524 526 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
525 527 defindent = indent + hang * ' '
526 528 text = ' '.join(map(str.strip, block['lines'][1:]))
527 529 return '%s\n%s\n' % (term, util.wrap(text, width=width,
528 530 initindent=defindent,
529 531 hangindent=defindent))
530 532 subindent = indent
531 533 if block['type'] == 'bullet':
532 534 if block['lines'][0].startswith('| '):
533 535 # Remove bullet for line blocks and add no extra
534 536 # indentation.
535 537 block['lines'][0] = block['lines'][0][2:]
536 538 else:
537 539 m = _bulletre.match(block['lines'][0])
538 540 subindent = indent + m.end() * ' '
539 541 elif block['type'] == 'field':
540 542 key = block['key']
541 543 subindent = indent + _fieldwidth * ' '
542 544 if len(key) + 2 > _fieldwidth:
543 545 # key too large, use full line width
544 546 key = key.ljust(width)
545 547 else:
546 548 # key fits within field width
547 549 key = key.ljust(_fieldwidth)
548 550 block['lines'][0] = key + block['lines'][0]
549 551 elif block['type'] == 'option':
550 552 return formatoption(block, width)
551 553
552 554 text = ' '.join(map(bytes.strip, block['lines']))
553 555 return util.wrap(text, width=width,
554 556 initindent=indent,
555 557 hangindent=subindent) + '\n'
556 558
557 559 def formathtml(blocks):
558 560 """Format RST blocks as HTML"""
559 561
560 562 out = []
561 563 headernest = ''
562 564 listnest = []
563 565
564 566 def escape(s):
565 567 return cgi.escape(s, True)
566 568
567 569 def openlist(start, level):
568 570 if not listnest or listnest[-1][0] != start:
569 571 listnest.append((start, level))
570 572 out.append('<%s>\n' % start)
571 573
572 574 blocks = [b for b in blocks if b['type'] != 'margin']
573 575
574 576 for pos, b in enumerate(blocks):
575 577 btype = b['type']
576 578 level = b['indent']
577 579 lines = b['lines']
578 580
579 581 if btype == 'admonition':
580 582 admonition = escape(_admonitiontitles[b['admonitiontitle']])
581 583 text = escape(' '.join(map(str.strip, lines)))
582 584 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
583 585 elif btype == 'paragraph':
584 586 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
585 587 elif btype == 'margin':
586 588 pass
587 589 elif btype == 'literal':
588 590 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
589 591 elif btype == 'section':
590 592 i = b['underline']
591 593 if i not in headernest:
592 594 headernest += i
593 595 level = headernest.index(i) + 1
594 596 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
595 597 elif btype == 'table':
596 598 table = b['table']
597 599 out.append('<table>\n')
598 600 for row in table:
599 601 out.append('<tr>')
600 602 for v in row:
601 603 out.append('<td>')
602 604 out.append(escape(v))
603 605 out.append('</td>')
604 606 out.append('\n')
605 607 out.pop()
606 608 out.append('</tr>\n')
607 609 out.append('</table>\n')
608 610 elif btype == 'definition':
609 611 openlist('dl', level)
610 612 term = escape(lines[0])
611 613 text = escape(' '.join(map(str.strip, lines[1:])))
612 614 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
613 615 elif btype == 'bullet':
614 616 bullet, head = lines[0].split(' ', 1)
615 617 if bullet in ('*', '-'):
616 618 openlist('ul', level)
617 619 else:
618 620 openlist('ol', level)
619 621 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
620 622 elif btype == 'field':
621 623 openlist('dl', level)
622 624 key = escape(b['key'])
623 625 text = escape(' '.join(map(str.strip, lines)))
624 626 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
625 627 elif btype == 'option':
626 628 openlist('dl', level)
627 629 opt = escape(b['optstr'])
628 630 desc = escape(' '.join(map(str.strip, lines)))
629 631 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
630 632
631 633 # close lists if indent level of next block is lower
632 634 if listnest:
633 635 start, level = listnest[-1]
634 636 if pos == len(blocks) - 1:
635 637 out.append('</%s>\n' % start)
636 638 listnest.pop()
637 639 else:
638 640 nb = blocks[pos + 1]
639 641 ni = nb['indent']
640 642 if (ni < level or
641 643 (ni == level and
642 644 nb['type'] not in 'definition bullet field option')):
643 645 out.append('</%s>\n' % start)
644 646 listnest.pop()
645 647
646 648 return ''.join(out)
647 649
648 650 def parse(text, indent=0, keep=None, admonitions=None):
649 651 """Parse text into a list of blocks"""
650 652 pruned = []
651 653 blocks = findblocks(text)
652 654 for b in blocks:
653 655 b['indent'] += indent
654 656 blocks = findliteralblocks(blocks)
655 657 blocks = findtables(blocks)
656 658 blocks, pruned = prunecontainers(blocks, keep or [])
657 659 blocks = findsections(blocks)
658 660 blocks = inlineliterals(blocks)
659 661 blocks = hgrole(blocks)
660 662 blocks = splitparagraphs(blocks)
661 663 blocks = updatefieldlists(blocks)
662 664 blocks = updateoptionlists(blocks)
663 665 blocks = findadmonitions(blocks, admonitions=admonitions)
664 666 blocks = addmargins(blocks)
665 667 blocks = prunecomments(blocks)
666 668 return blocks, pruned
667 669
668 670 def formatblocks(blocks, width):
669 671 text = ''.join(formatblock(b, width) for b in blocks)
670 672 return text
671 673
672 674 def format(text, width=80, indent=0, keep=None, style='plain', section=None):
673 675 """Parse and format the text according to width."""
674 676 blocks, pruned = parse(text, indent, keep or [])
675 677 parents = []
676 678 if section:
677 679 sections = getsections(blocks)
678 680 blocks = []
679 681 i = 0
680 682 lastparents = []
681 683 synthetic = []
682 684 collapse = True
683 685 while i < len(sections):
684 686 name, nest, b = sections[i]
685 687 del parents[nest:]
686 688 parents.append(i)
687 689 if name == section:
688 690 if lastparents != parents:
689 691 llen = len(lastparents)
690 692 plen = len(parents)
691 693 if llen and llen != plen:
692 694 collapse = False
693 695 s = []
694 696 for j in xrange(3, plen - 1):
695 697 parent = parents[j]
696 698 if (j >= llen or
697 699 lastparents[j] != parent):
698 700 s.append(len(blocks))
699 701 sec = sections[parent][2]
700 702 blocks.append(sec[0])
701 703 blocks.append(sec[-1])
702 704 if s:
703 705 synthetic.append(s)
704 706
705 707 lastparents = parents[:]
706 708 blocks.extend(b)
707 709
708 710 ## Also show all subnested sections
709 711 while i + 1 < len(sections) and sections[i + 1][1] > nest:
710 712 i += 1
711 713 blocks.extend(sections[i][2])
712 714 i += 1
713 715 if collapse:
714 716 synthetic.reverse()
715 717 for s in synthetic:
716 718 path = [blocks[syn]['lines'][0] for syn in s]
717 719 real = s[-1] + 2
718 720 realline = blocks[real]['lines']
719 721 realline[0] = ('"%s"' %
720 722 '.'.join(path + [realline[0]]).replace('"', ''))
721 723 del blocks[s[0]:real]
722 724
723 725 if style == 'html':
724 726 text = formathtml(blocks)
725 727 else:
726 728 text = ''.join(formatblock(b, width) for b in blocks)
727 729 if keep is None:
728 730 return text
729 731 else:
730 732 return text, pruned
731 733
732 734 def getsections(blocks):
733 735 '''return a list of (section name, nesting level, blocks) tuples'''
734 736 nest = ""
735 737 level = 0
736 738 secs = []
737 739
738 740 def getname(b):
739 741 if b['type'] == 'field':
740 742 x = b['key']
741 743 else:
742 744 x = b['lines'][0]
743 745 x = encoding.lower(x).strip('"')
744 746 if '(' in x:
745 747 x = x.split('(')[0]
746 748 return x
747 749
748 750 for b in blocks:
749 751 if b['type'] == 'section':
750 752 i = b['underline']
751 753 if i not in nest:
752 754 nest += i
753 755 level = nest.index(i) + 1
754 756 nest = nest[:level]
755 757 secs.append((getname(b), level, [b]))
756 758 elif b['type'] in ('definition', 'field'):
757 759 i = ' '
758 760 if i not in nest:
759 761 nest += i
760 762 level = nest.index(i) + 1
761 763 nest = nest[:level]
762 764 for i in range(1, len(secs) + 1):
763 765 sec = secs[-i]
764 766 if sec[1] < level:
765 767 break
766 768 siblings = [a for a in sec[2] if a['type'] == 'definition']
767 769 if siblings:
768 770 siblingindent = siblings[-1]['indent']
769 771 indent = b['indent']
770 772 if siblingindent < indent:
771 773 level += 1
772 774 break
773 775 elif siblingindent == indent:
774 776 level = sec[1]
775 777 break
776 778 secs.append((getname(b), level, [b]))
777 779 else:
778 780 if not secs:
779 781 # add an initial empty section
780 782 secs = [('', 0, [])]
781 783 if b['type'] != 'margin':
782 784 pointer = 1
783 785 bindent = b['indent']
784 786 while pointer < len(secs):
785 787 section = secs[-pointer][2][0]
786 788 if section['type'] != 'margin':
787 789 sindent = section['indent']
788 790 if len(section['lines']) > 1:
789 791 sindent += len(section['lines'][1]) - \
790 792 len(section['lines'][1].lstrip(' '))
791 793 if bindent >= sindent:
792 794 break
793 795 pointer += 1
794 796 if pointer > 1:
795 797 blevel = secs[-pointer][1]
796 798 if section['type'] != b['type']:
797 799 blevel += 1
798 800 secs.append(('', blevel, []))
799 801 secs[-1][2].append(b)
800 802 return secs
801 803
802 804 def decorateblocks(blocks, width):
803 805 '''generate a list of (section name, line text) pairs for search'''
804 806 lines = []
805 807 for s in getsections(blocks):
806 808 section = s[0]
807 809 text = formatblocks(s[2], width)
808 810 lines.append([(section, l) for l in text.splitlines(True)])
809 811 return lines
810 812
811 813 def maketable(data, indent=0, header=False):
812 814 '''Generate an RST table for the given table data as a list of lines'''
813 815
814 816 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
815 817 indent = ' ' * indent
816 818 div = indent + ' '.join('=' * w for w in widths) + '\n'
817 819
818 820 out = [div]
819 821 for row in data:
820 822 l = []
821 823 for w, v in zip(widths, row):
822 824 if '\n' in v:
823 825 # only remove line breaks and indentation, long lines are
824 826 # handled by the next tool
825 827 v = ' '.join(e.lstrip() for e in v.split('\n'))
826 828 pad = ' ' * (w - encoding.colwidth(v))
827 829 l.append(v + pad)
828 830 out.append(indent + ' '.join(l) + "\n")
829 831 if header and len(data) > 1:
830 832 out.insert(2, div)
831 833 out.append(div)
832 834 return out
General Comments 0
You need to be logged in to leave comments. Login now