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