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