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