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