##// END OF EJS Templates
minirst: make format() simply return a formatted text...
Yuya Nishihara -
r39346:a2a5d4ad default
parent child Browse files
Show More
@@ -1,835 +1,831 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 re
24 24
25 25 from .i18n import _
26 26 from . import (
27 27 encoding,
28 28 pycompat,
29 29 url,
30 30 )
31 31 from .utils import (
32 32 stringutil,
33 33 )
34 34
35 35 def section(s):
36 36 return "%s\n%s\n\n" % (s, "\"" * encoding.colwidth(s))
37 37
38 38 def subsection(s):
39 39 return "%s\n%s\n\n" % (s, '=' * encoding.colwidth(s))
40 40
41 41 def subsubsection(s):
42 42 return "%s\n%s\n\n" % (s, "-" * encoding.colwidth(s))
43 43
44 44 def subsubsubsection(s):
45 45 return "%s\n%s\n\n" % (s, "." * encoding.colwidth(s))
46 46
47 47 def replace(text, substs):
48 48 '''
49 49 Apply a list of (find, replace) pairs to a text.
50 50
51 51 >>> replace(b"foo bar", [(b'f', b'F'), (b'b', b'B')])
52 52 'Foo Bar'
53 53 >>> encoding.encoding = b'latin1'
54 54 >>> replace(b'\\x81\\\\', [(b'\\\\', b'/')])
55 55 '\\x81/'
56 56 >>> encoding.encoding = b'shiftjis'
57 57 >>> replace(b'\\x81\\\\', [(b'\\\\', b'/')])
58 58 '\\x81\\\\'
59 59 '''
60 60
61 61 # some character encodings (cp932 for Japanese, at least) use
62 62 # ASCII characters other than control/alphabet/digit as a part of
63 63 # multi-bytes characters, so direct replacing with such characters
64 64 # on strings in local encoding causes invalid byte sequences.
65 65 utext = text.decode(pycompat.sysstr(encoding.encoding))
66 66 for f, t in substs:
67 67 utext = utext.replace(f.decode("ascii"), t.decode("ascii"))
68 68 return utext.encode(pycompat.sysstr(encoding.encoding))
69 69
70 70 _blockre = re.compile(br"\n(?:\s*\n)+")
71 71
72 72 def findblocks(text):
73 73 """Find continuous blocks of lines in text.
74 74
75 75 Returns a list of dictionaries representing the blocks. Each block
76 76 has an 'indent' field and a 'lines' field.
77 77 """
78 78 blocks = []
79 79 for b in _blockre.split(text.lstrip('\n').rstrip()):
80 80 lines = b.splitlines()
81 81 if lines:
82 82 indent = min((len(l) - len(l.lstrip())) for l in lines)
83 83 lines = [l[indent:] for l in lines]
84 84 blocks.append({'indent': indent, 'lines': lines})
85 85 return blocks
86 86
87 87 def findliteralblocks(blocks):
88 88 """Finds literal blocks and adds a 'type' field to the blocks.
89 89
90 90 Literal blocks are given the type 'literal', all other blocks are
91 91 given type the 'paragraph'.
92 92 """
93 93 i = 0
94 94 while i < len(blocks):
95 95 # Searching for a block that looks like this:
96 96 #
97 97 # +------------------------------+
98 98 # | paragraph |
99 99 # | (ends with "::") |
100 100 # +------------------------------+
101 101 # +---------------------------+
102 102 # | indented literal block |
103 103 # +---------------------------+
104 104 blocks[i]['type'] = 'paragraph'
105 105 if blocks[i]['lines'][-1].endswith('::') and i + 1 < len(blocks):
106 106 indent = blocks[i]['indent']
107 107 adjustment = blocks[i + 1]['indent'] - indent
108 108
109 109 if blocks[i]['lines'] == ['::']:
110 110 # Expanded form: remove block
111 111 del blocks[i]
112 112 i -= 1
113 113 elif blocks[i]['lines'][-1].endswith(' ::'):
114 114 # Partially minimized form: remove space and both
115 115 # colons.
116 116 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-3]
117 117 elif len(blocks[i]['lines']) == 1 and \
118 118 blocks[i]['lines'][0].lstrip(' ').startswith('.. ') and \
119 119 blocks[i]['lines'][0].find(' ', 3) == -1:
120 120 # directive on its own line, not a literal block
121 121 i += 1
122 122 continue
123 123 else:
124 124 # Fully minimized form: remove just one colon.
125 125 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-1]
126 126
127 127 # List items are formatted with a hanging indent. We must
128 128 # correct for this here while we still have the original
129 129 # information on the indentation of the subsequent literal
130 130 # blocks available.
131 131 m = _bulletre.match(blocks[i]['lines'][0])
132 132 if m:
133 133 indent += m.end()
134 134 adjustment -= m.end()
135 135
136 136 # Mark the following indented blocks.
137 137 while i + 1 < len(blocks) and blocks[i + 1]['indent'] > indent:
138 138 blocks[i + 1]['type'] = 'literal'
139 139 blocks[i + 1]['indent'] -= adjustment
140 140 i += 1
141 141 i += 1
142 142 return blocks
143 143
144 144 _bulletre = re.compile(br'(\*|-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ')
145 145 _optionre = re.compile(br'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)'
146 146 br'((.*) +)(.*)$')
147 147 _fieldre = re.compile(br':(?![: ])([^:]*)(?<! ):[ ]+(.*)')
148 148 _definitionre = re.compile(br'[^ ]')
149 149 _tablere = re.compile(br'(=+\s+)*=+')
150 150
151 151 def splitparagraphs(blocks):
152 152 """Split paragraphs into lists."""
153 153 # Tuples with (list type, item regexp, single line items?). Order
154 154 # matters: definition lists has the least specific regexp and must
155 155 # come last.
156 156 listtypes = [('bullet', _bulletre, True),
157 157 ('option', _optionre, True),
158 158 ('field', _fieldre, True),
159 159 ('definition', _definitionre, False)]
160 160
161 161 def match(lines, i, itemre, singleline):
162 162 """Does itemre match an item at line i?
163 163
164 164 A list item can be followed by an indented line or another list
165 165 item (but only if singleline is True).
166 166 """
167 167 line1 = lines[i]
168 168 line2 = i + 1 < len(lines) and lines[i + 1] or ''
169 169 if not itemre.match(line1):
170 170 return False
171 171 if singleline:
172 172 return line2 == '' or line2[0:1] == ' ' or itemre.match(line2)
173 173 else:
174 174 return line2.startswith(' ')
175 175
176 176 i = 0
177 177 while i < len(blocks):
178 178 if blocks[i]['type'] == 'paragraph':
179 179 lines = blocks[i]['lines']
180 180 for type, itemre, singleline in listtypes:
181 181 if match(lines, 0, itemre, singleline):
182 182 items = []
183 183 for j, line in enumerate(lines):
184 184 if match(lines, j, itemre, singleline):
185 185 items.append({'type': type, 'lines': [],
186 186 'indent': blocks[i]['indent']})
187 187 items[-1]['lines'].append(line)
188 188 blocks[i:i + 1] = items
189 189 break
190 190 i += 1
191 191 return blocks
192 192
193 193 _fieldwidth = 14
194 194
195 195 def updatefieldlists(blocks):
196 196 """Find key for field lists."""
197 197 i = 0
198 198 while i < len(blocks):
199 199 if blocks[i]['type'] != 'field':
200 200 i += 1
201 201 continue
202 202
203 203 j = i
204 204 while j < len(blocks) and blocks[j]['type'] == 'field':
205 205 m = _fieldre.match(blocks[j]['lines'][0])
206 206 key, rest = m.groups()
207 207 blocks[j]['lines'][0] = rest
208 208 blocks[j]['key'] = key
209 209 j += 1
210 210
211 211 i = j + 1
212 212
213 213 return blocks
214 214
215 215 def updateoptionlists(blocks):
216 216 i = 0
217 217 while i < len(blocks):
218 218 if blocks[i]['type'] != 'option':
219 219 i += 1
220 220 continue
221 221
222 222 optstrwidth = 0
223 223 j = i
224 224 while j < len(blocks) and blocks[j]['type'] == 'option':
225 225 m = _optionre.match(blocks[j]['lines'][0])
226 226
227 227 shortoption = m.group(2)
228 228 group3 = m.group(3)
229 229 longoption = group3[2:].strip()
230 230 desc = m.group(6).strip()
231 231 longoptionarg = m.group(5).strip()
232 232 blocks[j]['lines'][0] = desc
233 233
234 234 noshortop = ''
235 235 if not shortoption:
236 236 noshortop = ' '
237 237
238 238 opt = "%s%s" % (shortoption and "-%s " % shortoption or '',
239 239 ("%s--%s %s") % (noshortop, longoption,
240 240 longoptionarg))
241 241 opt = opt.rstrip()
242 242 blocks[j]['optstr'] = opt
243 243 optstrwidth = max(optstrwidth, encoding.colwidth(opt))
244 244 j += 1
245 245
246 246 for block in blocks[i:j]:
247 247 block['optstrwidth'] = optstrwidth
248 248 i = j + 1
249 249 return blocks
250 250
251 251 def prunecontainers(blocks, keep):
252 252 """Prune unwanted containers.
253 253
254 254 The blocks must have a 'type' field, i.e., they should have been
255 255 run through findliteralblocks first.
256 256 """
257 257 pruned = []
258 258 i = 0
259 259 while i + 1 < len(blocks):
260 260 # Searching for a block that looks like this:
261 261 #
262 262 # +-------+---------------------------+
263 263 # | ".. container ::" type |
264 264 # +---+ |
265 265 # | blocks |
266 266 # +-------------------------------+
267 267 if (blocks[i]['type'] == 'paragraph' and
268 268 blocks[i]['lines'][0].startswith('.. container::')):
269 269 indent = blocks[i]['indent']
270 270 adjustment = blocks[i + 1]['indent'] - indent
271 271 containertype = blocks[i]['lines'][0][15:]
272 272 prune = True
273 273 for c in keep:
274 274 if c in containertype.split('.'):
275 275 prune = False
276 276 if prune:
277 277 pruned.append(containertype)
278 278
279 279 # Always delete "..container:: type" block
280 280 del blocks[i]
281 281 j = i
282 282 i -= 1
283 283 while j < len(blocks) and blocks[j]['indent'] > indent:
284 284 if prune:
285 285 del blocks[j]
286 286 else:
287 287 blocks[j]['indent'] -= adjustment
288 288 j += 1
289 289 i += 1
290 290 return blocks, pruned
291 291
292 292 _sectionre = re.compile(br"""^([-=`:.'"~^_*+#])\1+$""")
293 293
294 294 def findtables(blocks):
295 295 '''Find simple tables
296 296
297 297 Only simple one-line table elements are supported
298 298 '''
299 299
300 300 for block in blocks:
301 301 # Searching for a block that looks like this:
302 302 #
303 303 # === ==== ===
304 304 # A B C
305 305 # === ==== === <- optional
306 306 # 1 2 3
307 307 # x y z
308 308 # === ==== ===
309 309 if (block['type'] == 'paragraph' and
310 310 len(block['lines']) > 2 and
311 311 _tablere.match(block['lines'][0]) and
312 312 block['lines'][0] == block['lines'][-1]):
313 313 block['type'] = 'table'
314 314 block['header'] = False
315 315 div = block['lines'][0]
316 316
317 317 # column markers are ASCII so we can calculate column
318 318 # position in bytes
319 319 columns = [x for x in pycompat.xrange(len(div))
320 320 if div[x:x + 1] == '=' and (x == 0 or
321 321 div[x - 1:x] == ' ')]
322 322 rows = []
323 323 for l in block['lines'][1:-1]:
324 324 if l == div:
325 325 block['header'] = True
326 326 continue
327 327 row = []
328 328 # we measure columns not in bytes or characters but in
329 329 # colwidth which makes things tricky
330 330 pos = columns[0] # leading whitespace is bytes
331 331 for n, start in enumerate(columns):
332 332 if n + 1 < len(columns):
333 333 width = columns[n + 1] - start
334 334 v = encoding.getcols(l, pos, width) # gather columns
335 335 pos += len(v) # calculate byte position of end
336 336 row.append(v.strip())
337 337 else:
338 338 row.append(l[pos:].strip())
339 339 rows.append(row)
340 340
341 341 block['table'] = rows
342 342
343 343 return blocks
344 344
345 345 def findsections(blocks):
346 346 """Finds sections.
347 347
348 348 The blocks must have a 'type' field, i.e., they should have been
349 349 run through findliteralblocks first.
350 350 """
351 351 for block in blocks:
352 352 # Searching for a block that looks like this:
353 353 #
354 354 # +------------------------------+
355 355 # | Section title |
356 356 # | ------------- |
357 357 # +------------------------------+
358 358 if (block['type'] == 'paragraph' and
359 359 len(block['lines']) == 2 and
360 360 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
361 361 _sectionre.match(block['lines'][1])):
362 362 block['underline'] = block['lines'][1][0:1]
363 363 block['type'] = 'section'
364 364 del block['lines'][1]
365 365 return blocks
366 366
367 367 def inlineliterals(blocks):
368 368 substs = [('``', '"')]
369 369 for b in blocks:
370 370 if b['type'] in ('paragraph', 'section'):
371 371 b['lines'] = [replace(l, substs) for l in b['lines']]
372 372 return blocks
373 373
374 374 def hgrole(blocks):
375 375 substs = [(':hg:`', "'hg "), ('`', "'")]
376 376 for b in blocks:
377 377 if b['type'] in ('paragraph', 'section'):
378 378 # Turn :hg:`command` into "hg command". This also works
379 379 # when there is a line break in the command and relies on
380 380 # the fact that we have no stray back-quotes in the input
381 381 # (run the blocks through inlineliterals first).
382 382 b['lines'] = [replace(l, substs) for l in b['lines']]
383 383 return blocks
384 384
385 385 def addmargins(blocks):
386 386 """Adds empty blocks for vertical spacing.
387 387
388 388 This groups bullets, options, and definitions together with no vertical
389 389 space between them, and adds an empty block between all other blocks.
390 390 """
391 391 i = 1
392 392 while i < len(blocks):
393 393 if (blocks[i]['type'] == blocks[i - 1]['type'] and
394 394 blocks[i]['type'] in ('bullet', 'option', 'field')):
395 395 i += 1
396 396 elif not blocks[i - 1]['lines']:
397 397 # no lines in previous block, do not separate
398 398 i += 1
399 399 else:
400 400 blocks.insert(i, {'lines': [''], 'indent': 0, 'type': 'margin'})
401 401 i += 2
402 402 return blocks
403 403
404 404 def prunecomments(blocks):
405 405 """Remove comments."""
406 406 i = 0
407 407 while i < len(blocks):
408 408 b = blocks[i]
409 409 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
410 410 b['lines'] == ['..']):
411 411 del blocks[i]
412 412 if i < len(blocks) and blocks[i]['type'] == 'margin':
413 413 del blocks[i]
414 414 else:
415 415 i += 1
416 416 return blocks
417 417
418 418
419 419 def findadmonitions(blocks, admonitions=None):
420 420 """
421 421 Makes the type of the block an admonition block if
422 422 the first line is an admonition directive
423 423 """
424 424 admonitions = admonitions or _admonitiontitles.keys()
425 425
426 426 admonitionre = re.compile(br'\.\. (%s)::' % '|'.join(sorted(admonitions)),
427 427 flags=re.IGNORECASE)
428 428
429 429 i = 0
430 430 while i < len(blocks):
431 431 m = admonitionre.match(blocks[i]['lines'][0])
432 432 if m:
433 433 blocks[i]['type'] = 'admonition'
434 434 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
435 435
436 436 firstline = blocks[i]['lines'][0][m.end() + 1:]
437 437 if firstline:
438 438 blocks[i]['lines'].insert(1, ' ' + firstline)
439 439
440 440 blocks[i]['admonitiontitle'] = admonitiontitle
441 441 del blocks[i]['lines'][0]
442 442 i = i + 1
443 443 return blocks
444 444
445 445 _admonitiontitles = {
446 446 'attention': _('Attention:'),
447 447 'caution': _('Caution:'),
448 448 'danger': _('!Danger!'),
449 449 'error': _('Error:'),
450 450 'hint': _('Hint:'),
451 451 'important': _('Important:'),
452 452 'note': _('Note:'),
453 453 'tip': _('Tip:'),
454 454 'warning': _('Warning!'),
455 455 }
456 456
457 457 def formatoption(block, width):
458 458 desc = ' '.join(map(bytes.strip, block['lines']))
459 459 colwidth = encoding.colwidth(block['optstr'])
460 460 usablewidth = width - 1
461 461 hanging = block['optstrwidth']
462 462 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
463 463 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
464 464 return ' %s\n' % (stringutil.wrap(desc, usablewidth,
465 465 initindent=initindent,
466 466 hangindent=hangindent))
467 467
468 468 def formatblock(block, width):
469 469 """Format a block according to width."""
470 470 if width <= 0:
471 471 width = 78
472 472 indent = ' ' * block['indent']
473 473 if block['type'] == 'admonition':
474 474 admonition = _admonitiontitles[block['admonitiontitle']]
475 475 if not block['lines']:
476 476 return indent + admonition + '\n'
477 477 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
478 478
479 479 defindent = indent + hang * ' '
480 480 text = ' '.join(map(bytes.strip, block['lines']))
481 481 return '%s\n%s\n' % (indent + admonition,
482 482 stringutil.wrap(text, width=width,
483 483 initindent=defindent,
484 484 hangindent=defindent))
485 485 if block['type'] == 'margin':
486 486 return '\n'
487 487 if block['type'] == 'literal':
488 488 indent += ' '
489 489 return indent + ('\n' + indent).join(block['lines']) + '\n'
490 490 if block['type'] == 'section':
491 491 underline = encoding.colwidth(block['lines'][0]) * block['underline']
492 492 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
493 493 if block['type'] == 'table':
494 494 table = block['table']
495 495 # compute column widths
496 496 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
497 497 text = ''
498 498 span = sum(widths) + len(widths) - 1
499 499 indent = ' ' * block['indent']
500 500 hang = ' ' * (len(indent) + span - widths[-1])
501 501
502 502 for row in table:
503 503 l = []
504 504 for w, v in zip(widths, row):
505 505 pad = ' ' * (w - encoding.colwidth(v))
506 506 l.append(v + pad)
507 507 l = ' '.join(l)
508 508 l = stringutil.wrap(l, width=width,
509 509 initindent=indent,
510 510 hangindent=hang)
511 511 if not text and block['header']:
512 512 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
513 513 else:
514 514 text += l + "\n"
515 515 return text
516 516 if block['type'] == 'definition':
517 517 term = indent + block['lines'][0]
518 518 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
519 519 defindent = indent + hang * ' '
520 520 text = ' '.join(map(bytes.strip, block['lines'][1:]))
521 521 return '%s\n%s\n' % (term, stringutil.wrap(text, width=width,
522 522 initindent=defindent,
523 523 hangindent=defindent))
524 524 subindent = indent
525 525 if block['type'] == 'bullet':
526 526 if block['lines'][0].startswith('| '):
527 527 # Remove bullet for line blocks and add no extra
528 528 # indentation.
529 529 block['lines'][0] = block['lines'][0][2:]
530 530 else:
531 531 m = _bulletre.match(block['lines'][0])
532 532 subindent = indent + m.end() * ' '
533 533 elif block['type'] == 'field':
534 534 key = block['key']
535 535 subindent = indent + _fieldwidth * ' '
536 536 if len(key) + 2 > _fieldwidth:
537 537 # key too large, use full line width
538 538 key = key.ljust(width)
539 539 else:
540 540 # key fits within field width
541 541 key = key.ljust(_fieldwidth)
542 542 block['lines'][0] = key + block['lines'][0]
543 543 elif block['type'] == 'option':
544 544 return formatoption(block, width)
545 545
546 546 text = ' '.join(map(bytes.strip, block['lines']))
547 547 return stringutil.wrap(text, width=width,
548 548 initindent=indent,
549 549 hangindent=subindent) + '\n'
550 550
551 551 def formathtml(blocks):
552 552 """Format RST blocks as HTML"""
553 553
554 554 out = []
555 555 headernest = ''
556 556 listnest = []
557 557
558 558 def escape(s):
559 559 return url.escape(s, True)
560 560
561 561 def openlist(start, level):
562 562 if not listnest or listnest[-1][0] != start:
563 563 listnest.append((start, level))
564 564 out.append('<%s>\n' % start)
565 565
566 566 blocks = [b for b in blocks if b['type'] != 'margin']
567 567
568 568 for pos, b in enumerate(blocks):
569 569 btype = b['type']
570 570 level = b['indent']
571 571 lines = b['lines']
572 572
573 573 if btype == 'admonition':
574 574 admonition = escape(_admonitiontitles[b['admonitiontitle']])
575 575 text = escape(' '.join(map(bytes.strip, lines)))
576 576 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
577 577 elif btype == 'paragraph':
578 578 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
579 579 elif btype == 'margin':
580 580 pass
581 581 elif btype == 'literal':
582 582 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
583 583 elif btype == 'section':
584 584 i = b['underline']
585 585 if i not in headernest:
586 586 headernest += i
587 587 level = headernest.index(i) + 1
588 588 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
589 589 elif btype == 'table':
590 590 table = b['table']
591 591 out.append('<table>\n')
592 592 for row in table:
593 593 out.append('<tr>')
594 594 for v in row:
595 595 out.append('<td>')
596 596 out.append(escape(v))
597 597 out.append('</td>')
598 598 out.append('\n')
599 599 out.pop()
600 600 out.append('</tr>\n')
601 601 out.append('</table>\n')
602 602 elif btype == 'definition':
603 603 openlist('dl', level)
604 604 term = escape(lines[0])
605 605 text = escape(' '.join(map(bytes.strip, lines[1:])))
606 606 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
607 607 elif btype == 'bullet':
608 608 bullet, head = lines[0].split(' ', 1)
609 609 if bullet in ('*', '-'):
610 610 openlist('ul', level)
611 611 else:
612 612 openlist('ol', level)
613 613 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
614 614 elif btype == 'field':
615 615 openlist('dl', level)
616 616 key = escape(b['key'])
617 617 text = escape(' '.join(map(bytes.strip, lines)))
618 618 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
619 619 elif btype == 'option':
620 620 openlist('dl', level)
621 621 opt = escape(b['optstr'])
622 622 desc = escape(' '.join(map(bytes.strip, lines)))
623 623 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
624 624
625 625 # close lists if indent level of next block is lower
626 626 if listnest:
627 627 start, level = listnest[-1]
628 628 if pos == len(blocks) - 1:
629 629 out.append('</%s>\n' % start)
630 630 listnest.pop()
631 631 else:
632 632 nb = blocks[pos + 1]
633 633 ni = nb['indent']
634 634 if (ni < level or
635 635 (ni == level and
636 636 nb['type'] not in 'definition bullet field option')):
637 637 out.append('</%s>\n' % start)
638 638 listnest.pop()
639 639
640 640 return ''.join(out)
641 641
642 642 def parse(text, indent=0, keep=None, admonitions=None):
643 643 """Parse text into a list of blocks"""
644 644 pruned = []
645 645 blocks = findblocks(text)
646 646 for b in blocks:
647 647 b['indent'] += indent
648 648 blocks = findliteralblocks(blocks)
649 649 blocks = findtables(blocks)
650 650 blocks, pruned = prunecontainers(blocks, keep or [])
651 651 blocks = findsections(blocks)
652 652 blocks = inlineliterals(blocks)
653 653 blocks = hgrole(blocks)
654 654 blocks = splitparagraphs(blocks)
655 655 blocks = updatefieldlists(blocks)
656 656 blocks = updateoptionlists(blocks)
657 657 blocks = findadmonitions(blocks, admonitions=admonitions)
658 658 blocks = addmargins(blocks)
659 659 blocks = prunecomments(blocks)
660 660 return blocks, pruned
661 661
662 662 def formatblocks(blocks, width):
663 663 text = ''.join(formatblock(b, width) for b in blocks)
664 664 return text
665 665
666 666 def formatplain(blocks, width):
667 667 """Format parsed blocks as plain text"""
668 668 return ''.join(formatblock(b, width) for b in blocks)
669 669
670 670 def format(text, width=80, indent=0, keep=None, style='plain', section=None):
671 671 """Parse and format the text according to width."""
672 672 blocks, pruned = parse(text, indent, keep or [])
673 673 if section:
674 674 blocks = filtersections(blocks, section)
675 675 if style == 'html':
676 text = formathtml(blocks)
676 return formathtml(blocks)
677 677 else:
678 text = formatplain(blocks, width=width)
679 if keep is None:
680 return text
681 else:
682 return text, pruned
678 return formatplain(blocks, width=width)
683 679
684 680 def filtersections(blocks, section):
685 681 """Select parsed blocks under the specified section"""
686 682 parents = []
687 683 sections = getsections(blocks)
688 684 blocks = []
689 685 i = 0
690 686 lastparents = []
691 687 synthetic = []
692 688 collapse = True
693 689 while i < len(sections):
694 690 name, nest, b = sections[i]
695 691 del parents[nest:]
696 692 parents.append(i)
697 693 if name == section:
698 694 if lastparents != parents:
699 695 llen = len(lastparents)
700 696 plen = len(parents)
701 697 if llen and llen != plen:
702 698 collapse = False
703 699 s = []
704 700 for j in pycompat.xrange(3, plen - 1):
705 701 parent = parents[j]
706 702 if (j >= llen or
707 703 lastparents[j] != parent):
708 704 s.append(len(blocks))
709 705 sec = sections[parent][2]
710 706 blocks.append(sec[0])
711 707 blocks.append(sec[-1])
712 708 if s:
713 709 synthetic.append(s)
714 710
715 711 lastparents = parents[:]
716 712 blocks.extend(b)
717 713
718 714 ## Also show all subnested sections
719 715 while i + 1 < len(sections) and sections[i + 1][1] > nest:
720 716 i += 1
721 717 blocks.extend(sections[i][2])
722 718 i += 1
723 719 if collapse:
724 720 synthetic.reverse()
725 721 for s in synthetic:
726 722 path = [blocks[syn]['lines'][0] for syn in s]
727 723 real = s[-1] + 2
728 724 realline = blocks[real]['lines']
729 725 realline[0] = ('"%s"' %
730 726 '.'.join(path + [realline[0]]).replace('"', ''))
731 727 del blocks[s[0]:real]
732 728
733 729 return blocks
734 730
735 731 def getsections(blocks):
736 732 '''return a list of (section name, nesting level, blocks) tuples'''
737 733 nest = ""
738 734 level = 0
739 735 secs = []
740 736
741 737 def getname(b):
742 738 if b['type'] == 'field':
743 739 x = b['key']
744 740 else:
745 741 x = b['lines'][0]
746 742 x = encoding.lower(x).strip('"')
747 743 if '(' in x:
748 744 x = x.split('(')[0]
749 745 return x
750 746
751 747 for b in blocks:
752 748 if b['type'] == 'section':
753 749 i = b['underline']
754 750 if i not in nest:
755 751 nest += i
756 752 level = nest.index(i) + 1
757 753 nest = nest[:level]
758 754 secs.append((getname(b), level, [b]))
759 755 elif b['type'] in ('definition', 'field'):
760 756 i = ' '
761 757 if i not in nest:
762 758 nest += i
763 759 level = nest.index(i) + 1
764 760 nest = nest[:level]
765 761 for i in range(1, len(secs) + 1):
766 762 sec = secs[-i]
767 763 if sec[1] < level:
768 764 break
769 765 siblings = [a for a in sec[2] if a['type'] == 'definition']
770 766 if siblings:
771 767 siblingindent = siblings[-1]['indent']
772 768 indent = b['indent']
773 769 if siblingindent < indent:
774 770 level += 1
775 771 break
776 772 elif siblingindent == indent:
777 773 level = sec[1]
778 774 break
779 775 secs.append((getname(b), level, [b]))
780 776 else:
781 777 if not secs:
782 778 # add an initial empty section
783 779 secs = [('', 0, [])]
784 780 if b['type'] != 'margin':
785 781 pointer = 1
786 782 bindent = b['indent']
787 783 while pointer < len(secs):
788 784 section = secs[-pointer][2][0]
789 785 if section['type'] != 'margin':
790 786 sindent = section['indent']
791 787 if len(section['lines']) > 1:
792 788 sindent += len(section['lines'][1]) - \
793 789 len(section['lines'][1].lstrip(' '))
794 790 if bindent >= sindent:
795 791 break
796 792 pointer += 1
797 793 if pointer > 1:
798 794 blevel = secs[-pointer][1]
799 795 if section['type'] != b['type']:
800 796 blevel += 1
801 797 secs.append(('', blevel, []))
802 798 secs[-1][2].append(b)
803 799 return secs
804 800
805 801 def decorateblocks(blocks, width):
806 802 '''generate a list of (section name, line text) pairs for search'''
807 803 lines = []
808 804 for s in getsections(blocks):
809 805 section = s[0]
810 806 text = formatblocks(s[2], width)
811 807 lines.append([(section, l) for l in text.splitlines(True)])
812 808 return lines
813 809
814 810 def maketable(data, indent=0, header=False):
815 811 '''Generate an RST table for the given table data as a list of lines'''
816 812
817 813 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
818 814 indent = ' ' * indent
819 815 div = indent + ' '.join('=' * w for w in widths) + '\n'
820 816
821 817 out = [div]
822 818 for row in data:
823 819 l = []
824 820 for w, v in zip(widths, row):
825 821 if '\n' in v:
826 822 # only remove line breaks and indentation, long lines are
827 823 # handled by the next tool
828 824 v = ' '.join(e.lstrip() for e in v.split('\n'))
829 825 pad = ' ' * (w - encoding.colwidth(v))
830 826 l.append(v + pad)
831 827 out.append(indent + ' '.join(l) + "\n")
832 828 if header and len(data) > 1:
833 829 out.insert(2, div)
834 830 out.append(div)
835 831 return out
@@ -1,718 +1,718 b''
1 1 # templatefuncs.py - common template functions
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import re
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 bin,
15 15 wdirid,
16 16 )
17 17 from . import (
18 18 color,
19 19 encoding,
20 20 error,
21 21 minirst,
22 22 obsutil,
23 23 registrar,
24 24 revset as revsetmod,
25 25 revsetlang,
26 26 scmutil,
27 27 templatefilters,
28 28 templatekw,
29 29 templateutil,
30 30 util,
31 31 )
32 32 from .utils import (
33 33 dateutil,
34 34 stringutil,
35 35 )
36 36
37 37 evalrawexp = templateutil.evalrawexp
38 38 evalwrapped = templateutil.evalwrapped
39 39 evalfuncarg = templateutil.evalfuncarg
40 40 evalboolean = templateutil.evalboolean
41 41 evaldate = templateutil.evaldate
42 42 evalinteger = templateutil.evalinteger
43 43 evalstring = templateutil.evalstring
44 44 evalstringliteral = templateutil.evalstringliteral
45 45
46 46 # dict of template built-in functions
47 47 funcs = {}
48 48 templatefunc = registrar.templatefunc(funcs)
49 49
50 50 @templatefunc('date(date[, fmt])')
51 51 def date(context, mapping, args):
52 52 """Format a date. See :hg:`help dates` for formatting
53 53 strings. The default is a Unix date format, including the timezone:
54 54 "Mon Sep 04 15:13:13 2006 0700"."""
55 55 if not (1 <= len(args) <= 2):
56 56 # i18n: "date" is a keyword
57 57 raise error.ParseError(_("date expects one or two arguments"))
58 58
59 59 date = evaldate(context, mapping, args[0],
60 60 # i18n: "date" is a keyword
61 61 _("date expects a date information"))
62 62 fmt = None
63 63 if len(args) == 2:
64 64 fmt = evalstring(context, mapping, args[1])
65 65 if fmt is None:
66 66 return dateutil.datestr(date)
67 67 else:
68 68 return dateutil.datestr(date, fmt)
69 69
70 70 @templatefunc('dict([[key=]value...])', argspec='*args **kwargs')
71 71 def dict_(context, mapping, args):
72 72 """Construct a dict from key-value pairs. A key may be omitted if
73 73 a value expression can provide an unambiguous name."""
74 74 data = util.sortdict()
75 75
76 76 for v in args['args']:
77 77 k = templateutil.findsymbolicname(v)
78 78 if not k:
79 79 raise error.ParseError(_('dict key cannot be inferred'))
80 80 if k in data or k in args['kwargs']:
81 81 raise error.ParseError(_("duplicated dict key '%s' inferred") % k)
82 82 data[k] = evalfuncarg(context, mapping, v)
83 83
84 84 data.update((k, evalfuncarg(context, mapping, v))
85 85 for k, v in args['kwargs'].iteritems())
86 86 return templateutil.hybriddict(data)
87 87
88 88 @templatefunc('diff([includepattern [, excludepattern]])', requires={'ctx'})
89 89 def diff(context, mapping, args):
90 90 """Show a diff, optionally
91 91 specifying files to include or exclude."""
92 92 if len(args) > 2:
93 93 # i18n: "diff" is a keyword
94 94 raise error.ParseError(_("diff expects zero, one, or two arguments"))
95 95
96 96 def getpatterns(i):
97 97 if i < len(args):
98 98 s = evalstring(context, mapping, args[i]).strip()
99 99 if s:
100 100 return [s]
101 101 return []
102 102
103 103 ctx = context.resource(mapping, 'ctx')
104 104 chunks = ctx.diff(match=ctx.match([], getpatterns(0), getpatterns(1)))
105 105
106 106 return ''.join(chunks)
107 107
108 108 @templatefunc('extdata(source)', argspec='source', requires={'ctx', 'cache'})
109 109 def extdata(context, mapping, args):
110 110 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
111 111 if 'source' not in args:
112 112 # i18n: "extdata" is a keyword
113 113 raise error.ParseError(_('extdata expects one argument'))
114 114
115 115 source = evalstring(context, mapping, args['source'])
116 116 if not source:
117 117 sym = templateutil.findsymbolicname(args['source'])
118 118 if sym:
119 119 raise error.ParseError(_('empty data source specified'),
120 120 hint=_("did you mean extdata('%s')?") % sym)
121 121 else:
122 122 raise error.ParseError(_('empty data source specified'))
123 123 cache = context.resource(mapping, 'cache').setdefault('extdata', {})
124 124 ctx = context.resource(mapping, 'ctx')
125 125 if source in cache:
126 126 data = cache[source]
127 127 else:
128 128 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
129 129 return data.get(ctx.rev(), '')
130 130
131 131 @templatefunc('files(pattern)', requires={'ctx'})
132 132 def files(context, mapping, args):
133 133 """All files of the current changeset matching the pattern. See
134 134 :hg:`help patterns`."""
135 135 if not len(args) == 1:
136 136 # i18n: "files" is a keyword
137 137 raise error.ParseError(_("files expects one argument"))
138 138
139 139 raw = evalstring(context, mapping, args[0])
140 140 ctx = context.resource(mapping, 'ctx')
141 141 m = ctx.match([raw])
142 142 files = list(ctx.matches(m))
143 143 return templateutil.compatlist(context, mapping, "file", files)
144 144
145 145 @templatefunc('fill(text[, width[, initialident[, hangindent]]])')
146 146 def fill(context, mapping, args):
147 147 """Fill many
148 148 paragraphs with optional indentation. See the "fill" filter."""
149 149 if not (1 <= len(args) <= 4):
150 150 # i18n: "fill" is a keyword
151 151 raise error.ParseError(_("fill expects one to four arguments"))
152 152
153 153 text = evalstring(context, mapping, args[0])
154 154 width = 76
155 155 initindent = ''
156 156 hangindent = ''
157 157 if 2 <= len(args) <= 4:
158 158 width = evalinteger(context, mapping, args[1],
159 159 # i18n: "fill" is a keyword
160 160 _("fill expects an integer width"))
161 161 try:
162 162 initindent = evalstring(context, mapping, args[2])
163 163 hangindent = evalstring(context, mapping, args[3])
164 164 except IndexError:
165 165 pass
166 166
167 167 return templatefilters.fill(text, width, initindent, hangindent)
168 168
169 169 @templatefunc('filter(iterable[, expr])')
170 170 def filter_(context, mapping, args):
171 171 """Remove empty elements from a list or a dict. If expr specified, it's
172 172 applied to each element to test emptiness."""
173 173 if not (1 <= len(args) <= 2):
174 174 # i18n: "filter" is a keyword
175 175 raise error.ParseError(_("filter expects one or two arguments"))
176 176 iterable = evalwrapped(context, mapping, args[0])
177 177 if len(args) == 1:
178 178 def select(w):
179 179 return w.tobool(context, mapping)
180 180 else:
181 181 def select(w):
182 182 if not isinstance(w, templateutil.mappable):
183 183 raise error.ParseError(_("not filterable by expression"))
184 184 lm = context.overlaymap(mapping, w.tomap(context))
185 185 return evalboolean(context, lm, args[1])
186 186 return iterable.filter(context, mapping, select)
187 187
188 188 @templatefunc('formatnode(node)', requires={'ui'})
189 189 def formatnode(context, mapping, args):
190 190 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
191 191 if len(args) != 1:
192 192 # i18n: "formatnode" is a keyword
193 193 raise error.ParseError(_("formatnode expects one argument"))
194 194
195 195 ui = context.resource(mapping, 'ui')
196 196 node = evalstring(context, mapping, args[0])
197 197 if ui.debugflag:
198 198 return node
199 199 return templatefilters.short(node)
200 200
201 201 @templatefunc('mailmap(author)', requires={'repo', 'cache'})
202 202 def mailmap(context, mapping, args):
203 203 """Return the author, updated according to the value
204 204 set in the .mailmap file"""
205 205 if len(args) != 1:
206 206 raise error.ParseError(_("mailmap expects one argument"))
207 207
208 208 author = evalstring(context, mapping, args[0])
209 209
210 210 cache = context.resource(mapping, 'cache')
211 211 repo = context.resource(mapping, 'repo')
212 212
213 213 if 'mailmap' not in cache:
214 214 data = repo.wvfs.tryread('.mailmap')
215 215 cache['mailmap'] = stringutil.parsemailmap(data)
216 216
217 217 return stringutil.mapname(cache['mailmap'], author)
218 218
219 219 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
220 220 argspec='text width fillchar left')
221 221 def pad(context, mapping, args):
222 222 """Pad text with a
223 223 fill character."""
224 224 if 'text' not in args or 'width' not in args:
225 225 # i18n: "pad" is a keyword
226 226 raise error.ParseError(_("pad() expects two to four arguments"))
227 227
228 228 width = evalinteger(context, mapping, args['width'],
229 229 # i18n: "pad" is a keyword
230 230 _("pad() expects an integer width"))
231 231
232 232 text = evalstring(context, mapping, args['text'])
233 233
234 234 left = False
235 235 fillchar = ' '
236 236 if 'fillchar' in args:
237 237 fillchar = evalstring(context, mapping, args['fillchar'])
238 238 if len(color.stripeffects(fillchar)) != 1:
239 239 # i18n: "pad" is a keyword
240 240 raise error.ParseError(_("pad() expects a single fill character"))
241 241 if 'left' in args:
242 242 left = evalboolean(context, mapping, args['left'])
243 243
244 244 fillwidth = width - encoding.colwidth(color.stripeffects(text))
245 245 if fillwidth <= 0:
246 246 return text
247 247 if left:
248 248 return fillchar * fillwidth + text
249 249 else:
250 250 return text + fillchar * fillwidth
251 251
252 252 @templatefunc('indent(text, indentchars[, firstline])')
253 253 def indent(context, mapping, args):
254 254 """Indents all non-empty lines
255 255 with the characters given in the indentchars string. An optional
256 256 third parameter will override the indent for the first line only
257 257 if present."""
258 258 if not (2 <= len(args) <= 3):
259 259 # i18n: "indent" is a keyword
260 260 raise error.ParseError(_("indent() expects two or three arguments"))
261 261
262 262 text = evalstring(context, mapping, args[0])
263 263 indent = evalstring(context, mapping, args[1])
264 264
265 265 if len(args) == 3:
266 266 firstline = evalstring(context, mapping, args[2])
267 267 else:
268 268 firstline = indent
269 269
270 270 # the indent function doesn't indent the first line, so we do it here
271 271 return templatefilters.indent(firstline + text, indent)
272 272
273 273 @templatefunc('get(dict, key)')
274 274 def get(context, mapping, args):
275 275 """Get an attribute/key from an object. Some keywords
276 276 are complex types. This function allows you to obtain the value of an
277 277 attribute on these types."""
278 278 if len(args) != 2:
279 279 # i18n: "get" is a keyword
280 280 raise error.ParseError(_("get() expects two arguments"))
281 281
282 282 dictarg = evalwrapped(context, mapping, args[0])
283 283 key = evalrawexp(context, mapping, args[1])
284 284 try:
285 285 return dictarg.getmember(context, mapping, key)
286 286 except error.ParseError as err:
287 287 # i18n: "get" is a keyword
288 288 hint = _("get() expects a dict as first argument")
289 289 raise error.ParseError(bytes(err), hint=hint)
290 290
291 291 @templatefunc('if(expr, then[, else])')
292 292 def if_(context, mapping, args):
293 293 """Conditionally execute based on the result of
294 294 an expression."""
295 295 if not (2 <= len(args) <= 3):
296 296 # i18n: "if" is a keyword
297 297 raise error.ParseError(_("if expects two or three arguments"))
298 298
299 299 test = evalboolean(context, mapping, args[0])
300 300 if test:
301 301 return evalrawexp(context, mapping, args[1])
302 302 elif len(args) == 3:
303 303 return evalrawexp(context, mapping, args[2])
304 304
305 305 @templatefunc('ifcontains(needle, haystack, then[, else])')
306 306 def ifcontains(context, mapping, args):
307 307 """Conditionally execute based
308 308 on whether the item "needle" is in "haystack"."""
309 309 if not (3 <= len(args) <= 4):
310 310 # i18n: "ifcontains" is a keyword
311 311 raise error.ParseError(_("ifcontains expects three or four arguments"))
312 312
313 313 haystack = evalwrapped(context, mapping, args[1])
314 314 try:
315 315 needle = evalrawexp(context, mapping, args[0])
316 316 found = haystack.contains(context, mapping, needle)
317 317 except error.ParseError:
318 318 found = False
319 319
320 320 if found:
321 321 return evalrawexp(context, mapping, args[2])
322 322 elif len(args) == 4:
323 323 return evalrawexp(context, mapping, args[3])
324 324
325 325 @templatefunc('ifeq(expr1, expr2, then[, else])')
326 326 def ifeq(context, mapping, args):
327 327 """Conditionally execute based on
328 328 whether 2 items are equivalent."""
329 329 if not (3 <= len(args) <= 4):
330 330 # i18n: "ifeq" is a keyword
331 331 raise error.ParseError(_("ifeq expects three or four arguments"))
332 332
333 333 test = evalstring(context, mapping, args[0])
334 334 match = evalstring(context, mapping, args[1])
335 335 if test == match:
336 336 return evalrawexp(context, mapping, args[2])
337 337 elif len(args) == 4:
338 338 return evalrawexp(context, mapping, args[3])
339 339
340 340 @templatefunc('join(list, sep)')
341 341 def join(context, mapping, args):
342 342 """Join items in a list with a delimiter."""
343 343 if not (1 <= len(args) <= 2):
344 344 # i18n: "join" is a keyword
345 345 raise error.ParseError(_("join expects one or two arguments"))
346 346
347 347 joinset = evalwrapped(context, mapping, args[0])
348 348 joiner = " "
349 349 if len(args) > 1:
350 350 joiner = evalstring(context, mapping, args[1])
351 351 return joinset.join(context, mapping, joiner)
352 352
353 353 @templatefunc('label(label, expr)', requires={'ui'})
354 354 def label(context, mapping, args):
355 355 """Apply a label to generated content. Content with
356 356 a label applied can result in additional post-processing, such as
357 357 automatic colorization."""
358 358 if len(args) != 2:
359 359 # i18n: "label" is a keyword
360 360 raise error.ParseError(_("label expects two arguments"))
361 361
362 362 ui = context.resource(mapping, 'ui')
363 363 thing = evalstring(context, mapping, args[1])
364 364 # preserve unknown symbol as literal so effects like 'red', 'bold',
365 365 # etc. don't need to be quoted
366 366 label = evalstringliteral(context, mapping, args[0])
367 367
368 368 return ui.label(thing, label)
369 369
370 370 @templatefunc('latesttag([pattern])')
371 371 def latesttag(context, mapping, args):
372 372 """The global tags matching the given pattern on the
373 373 most recent globally tagged ancestor of this changeset.
374 374 If no such tags exist, the "{tag}" template resolves to
375 375 the string "null". See :hg:`help revisions.patterns` for the pattern
376 376 syntax.
377 377 """
378 378 if len(args) > 1:
379 379 # i18n: "latesttag" is a keyword
380 380 raise error.ParseError(_("latesttag expects at most one argument"))
381 381
382 382 pattern = None
383 383 if len(args) == 1:
384 384 pattern = evalstring(context, mapping, args[0])
385 385 return templatekw.showlatesttags(context, mapping, pattern)
386 386
387 387 @templatefunc('localdate(date[, tz])')
388 388 def localdate(context, mapping, args):
389 389 """Converts a date to the specified timezone.
390 390 The default is local date."""
391 391 if not (1 <= len(args) <= 2):
392 392 # i18n: "localdate" is a keyword
393 393 raise error.ParseError(_("localdate expects one or two arguments"))
394 394
395 395 date = evaldate(context, mapping, args[0],
396 396 # i18n: "localdate" is a keyword
397 397 _("localdate expects a date information"))
398 398 if len(args) >= 2:
399 399 tzoffset = None
400 400 tz = evalfuncarg(context, mapping, args[1])
401 401 if isinstance(tz, bytes):
402 402 tzoffset, remainder = dateutil.parsetimezone(tz)
403 403 if remainder:
404 404 tzoffset = None
405 405 if tzoffset is None:
406 406 try:
407 407 tzoffset = int(tz)
408 408 except (TypeError, ValueError):
409 409 # i18n: "localdate" is a keyword
410 410 raise error.ParseError(_("localdate expects a timezone"))
411 411 else:
412 412 tzoffset = dateutil.makedate()[1]
413 413 return templateutil.date((date[0], tzoffset))
414 414
415 415 @templatefunc('max(iterable)')
416 416 def max_(context, mapping, args, **kwargs):
417 417 """Return the max of an iterable"""
418 418 if len(args) != 1:
419 419 # i18n: "max" is a keyword
420 420 raise error.ParseError(_("max expects one argument"))
421 421
422 422 iterable = evalwrapped(context, mapping, args[0])
423 423 try:
424 424 return iterable.getmax(context, mapping)
425 425 except error.ParseError as err:
426 426 # i18n: "max" is a keyword
427 427 hint = _("max first argument should be an iterable")
428 428 raise error.ParseError(bytes(err), hint=hint)
429 429
430 430 @templatefunc('min(iterable)')
431 431 def min_(context, mapping, args, **kwargs):
432 432 """Return the min of an iterable"""
433 433 if len(args) != 1:
434 434 # i18n: "min" is a keyword
435 435 raise error.ParseError(_("min expects one argument"))
436 436
437 437 iterable = evalwrapped(context, mapping, args[0])
438 438 try:
439 439 return iterable.getmin(context, mapping)
440 440 except error.ParseError as err:
441 441 # i18n: "min" is a keyword
442 442 hint = _("min first argument should be an iterable")
443 443 raise error.ParseError(bytes(err), hint=hint)
444 444
445 445 @templatefunc('mod(a, b)')
446 446 def mod(context, mapping, args):
447 447 """Calculate a mod b such that a / b + a mod b == a"""
448 448 if not len(args) == 2:
449 449 # i18n: "mod" is a keyword
450 450 raise error.ParseError(_("mod expects two arguments"))
451 451
452 452 func = lambda a, b: a % b
453 453 return templateutil.runarithmetic(context, mapping,
454 454 (func, args[0], args[1]))
455 455
456 456 @templatefunc('obsfateoperations(markers)')
457 457 def obsfateoperations(context, mapping, args):
458 458 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
459 459 if len(args) != 1:
460 460 # i18n: "obsfateoperations" is a keyword
461 461 raise error.ParseError(_("obsfateoperations expects one argument"))
462 462
463 463 markers = evalfuncarg(context, mapping, args[0])
464 464
465 465 try:
466 466 data = obsutil.markersoperations(markers)
467 467 return templateutil.hybridlist(data, name='operation')
468 468 except (TypeError, KeyError):
469 469 # i18n: "obsfateoperations" is a keyword
470 470 errmsg = _("obsfateoperations first argument should be an iterable")
471 471 raise error.ParseError(errmsg)
472 472
473 473 @templatefunc('obsfatedate(markers)')
474 474 def obsfatedate(context, mapping, args):
475 475 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
476 476 if len(args) != 1:
477 477 # i18n: "obsfatedate" is a keyword
478 478 raise error.ParseError(_("obsfatedate expects one argument"))
479 479
480 480 markers = evalfuncarg(context, mapping, args[0])
481 481
482 482 try:
483 483 # TODO: maybe this has to be a wrapped list of date wrappers?
484 484 data = obsutil.markersdates(markers)
485 485 return templateutil.hybridlist(data, name='date', fmt='%d %d')
486 486 except (TypeError, KeyError):
487 487 # i18n: "obsfatedate" is a keyword
488 488 errmsg = _("obsfatedate first argument should be an iterable")
489 489 raise error.ParseError(errmsg)
490 490
491 491 @templatefunc('obsfateusers(markers)')
492 492 def obsfateusers(context, mapping, args):
493 493 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
494 494 if len(args) != 1:
495 495 # i18n: "obsfateusers" is a keyword
496 496 raise error.ParseError(_("obsfateusers expects one argument"))
497 497
498 498 markers = evalfuncarg(context, mapping, args[0])
499 499
500 500 try:
501 501 data = obsutil.markersusers(markers)
502 502 return templateutil.hybridlist(data, name='user')
503 503 except (TypeError, KeyError, ValueError):
504 504 # i18n: "obsfateusers" is a keyword
505 505 msg = _("obsfateusers first argument should be an iterable of "
506 506 "obsmakers")
507 507 raise error.ParseError(msg)
508 508
509 509 @templatefunc('obsfateverb(successors, markers)')
510 510 def obsfateverb(context, mapping, args):
511 511 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
512 512 if len(args) != 2:
513 513 # i18n: "obsfateverb" is a keyword
514 514 raise error.ParseError(_("obsfateverb expects two arguments"))
515 515
516 516 successors = evalfuncarg(context, mapping, args[0])
517 517 markers = evalfuncarg(context, mapping, args[1])
518 518
519 519 try:
520 520 return obsutil.obsfateverb(successors, markers)
521 521 except TypeError:
522 522 # i18n: "obsfateverb" is a keyword
523 523 errmsg = _("obsfateverb first argument should be countable")
524 524 raise error.ParseError(errmsg)
525 525
526 526 @templatefunc('relpath(path)', requires={'repo'})
527 527 def relpath(context, mapping, args):
528 528 """Convert a repository-absolute path into a filesystem path relative to
529 529 the current working directory."""
530 530 if len(args) != 1:
531 531 # i18n: "relpath" is a keyword
532 532 raise error.ParseError(_("relpath expects one argument"))
533 533
534 534 repo = context.resource(mapping, 'repo')
535 535 path = evalstring(context, mapping, args[0])
536 536 return repo.pathto(path)
537 537
538 538 @templatefunc('revset(query[, formatargs...])', requires={'repo', 'cache'})
539 539 def revset(context, mapping, args):
540 540 """Execute a revision set query. See
541 541 :hg:`help revset`."""
542 542 if not len(args) > 0:
543 543 # i18n: "revset" is a keyword
544 544 raise error.ParseError(_("revset expects one or more arguments"))
545 545
546 546 raw = evalstring(context, mapping, args[0])
547 547 repo = context.resource(mapping, 'repo')
548 548
549 549 def query(expr):
550 550 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
551 551 return m(repo)
552 552
553 553 if len(args) > 1:
554 554 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
555 555 revs = query(revsetlang.formatspec(raw, *formatargs))
556 556 revs = list(revs)
557 557 else:
558 558 cache = context.resource(mapping, 'cache')
559 559 revsetcache = cache.setdefault("revsetcache", {})
560 560 if raw in revsetcache:
561 561 revs = revsetcache[raw]
562 562 else:
563 563 revs = query(raw)
564 564 revs = list(revs)
565 565 revsetcache[raw] = revs
566 566 return templatekw.showrevslist(context, mapping, "revision", revs)
567 567
568 568 @templatefunc('rstdoc(text, style)')
569 569 def rstdoc(context, mapping, args):
570 570 """Format reStructuredText."""
571 571 if len(args) != 2:
572 572 # i18n: "rstdoc" is a keyword
573 573 raise error.ParseError(_("rstdoc expects two arguments"))
574 574
575 575 text = evalstring(context, mapping, args[0])
576 576 style = evalstring(context, mapping, args[1])
577 577
578 return minirst.format(text, style=style, keep=['verbose'])[0]
578 return minirst.format(text, style=style, keep=['verbose'])
579 579
580 580 @templatefunc('separate(sep, args...)', argspec='sep *args')
581 581 def separate(context, mapping, args):
582 582 """Add a separator between non-empty arguments."""
583 583 if 'sep' not in args:
584 584 # i18n: "separate" is a keyword
585 585 raise error.ParseError(_("separate expects at least one argument"))
586 586
587 587 sep = evalstring(context, mapping, args['sep'])
588 588 first = True
589 589 for arg in args['args']:
590 590 argstr = evalstring(context, mapping, arg)
591 591 if not argstr:
592 592 continue
593 593 if first:
594 594 first = False
595 595 else:
596 596 yield sep
597 597 yield argstr
598 598
599 599 @templatefunc('shortest(node, minlength=4)', requires={'repo', 'cache'})
600 600 def shortest(context, mapping, args):
601 601 """Obtain the shortest representation of
602 602 a node."""
603 603 if not (1 <= len(args) <= 2):
604 604 # i18n: "shortest" is a keyword
605 605 raise error.ParseError(_("shortest() expects one or two arguments"))
606 606
607 607 hexnode = evalstring(context, mapping, args[0])
608 608
609 609 minlength = 4
610 610 if len(args) > 1:
611 611 minlength = evalinteger(context, mapping, args[1],
612 612 # i18n: "shortest" is a keyword
613 613 _("shortest() expects an integer minlength"))
614 614
615 615 repo = context.resource(mapping, 'repo')
616 616 if len(hexnode) > 40:
617 617 return hexnode
618 618 elif len(hexnode) == 40:
619 619 try:
620 620 node = bin(hexnode)
621 621 except TypeError:
622 622 return hexnode
623 623 else:
624 624 try:
625 625 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
626 626 except error.WdirUnsupported:
627 627 node = wdirid
628 628 except error.LookupError:
629 629 return hexnode
630 630 if not node:
631 631 return hexnode
632 632 cache = context.resource(mapping, 'cache')
633 633 try:
634 634 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
635 635 except error.RepoLookupError:
636 636 return hexnode
637 637
638 638 @templatefunc('strip(text[, chars])')
639 639 def strip(context, mapping, args):
640 640 """Strip characters from a string. By default,
641 641 strips all leading and trailing whitespace."""
642 642 if not (1 <= len(args) <= 2):
643 643 # i18n: "strip" is a keyword
644 644 raise error.ParseError(_("strip expects one or two arguments"))
645 645
646 646 text = evalstring(context, mapping, args[0])
647 647 if len(args) == 2:
648 648 chars = evalstring(context, mapping, args[1])
649 649 return text.strip(chars)
650 650 return text.strip()
651 651
652 652 @templatefunc('sub(pattern, replacement, expression)')
653 653 def sub(context, mapping, args):
654 654 """Perform text substitution
655 655 using regular expressions."""
656 656 if len(args) != 3:
657 657 # i18n: "sub" is a keyword
658 658 raise error.ParseError(_("sub expects three arguments"))
659 659
660 660 pat = evalstring(context, mapping, args[0])
661 661 rpl = evalstring(context, mapping, args[1])
662 662 src = evalstring(context, mapping, args[2])
663 663 try:
664 664 patre = re.compile(pat)
665 665 except re.error:
666 666 # i18n: "sub" is a keyword
667 667 raise error.ParseError(_("sub got an invalid pattern: %s") % pat)
668 668 try:
669 669 yield patre.sub(rpl, src)
670 670 except re.error:
671 671 # i18n: "sub" is a keyword
672 672 raise error.ParseError(_("sub got an invalid replacement: %s") % rpl)
673 673
674 674 @templatefunc('startswith(pattern, text)')
675 675 def startswith(context, mapping, args):
676 676 """Returns the value from the "text" argument
677 677 if it begins with the content from the "pattern" argument."""
678 678 if len(args) != 2:
679 679 # i18n: "startswith" is a keyword
680 680 raise error.ParseError(_("startswith expects two arguments"))
681 681
682 682 patn = evalstring(context, mapping, args[0])
683 683 text = evalstring(context, mapping, args[1])
684 684 if text.startswith(patn):
685 685 return text
686 686 return ''
687 687
688 688 @templatefunc('word(number, text[, separator])')
689 689 def word(context, mapping, args):
690 690 """Return the nth word from a string."""
691 691 if not (2 <= len(args) <= 3):
692 692 # i18n: "word" is a keyword
693 693 raise error.ParseError(_("word expects two or three arguments, got %d")
694 694 % len(args))
695 695
696 696 num = evalinteger(context, mapping, args[0],
697 697 # i18n: "word" is a keyword
698 698 _("word expects an integer index"))
699 699 text = evalstring(context, mapping, args[1])
700 700 if len(args) == 3:
701 701 splitter = evalstring(context, mapping, args[2])
702 702 else:
703 703 splitter = None
704 704
705 705 tokens = text.split(splitter)
706 706 if num >= len(tokens) or num < -len(tokens):
707 707 return ''
708 708 else:
709 709 return tokens[num]
710 710
711 711 def loadfunction(ui, extname, registrarobj):
712 712 """Load template function from specified registrarobj
713 713 """
714 714 for name, func in registrarobj._table.iteritems():
715 715 funcs[name] = func
716 716
717 717 # tell hggettext to extract docstrings from these functions:
718 718 i18nfunctions = funcs.values()
@@ -1,268 +1,267 b''
1 1 from __future__ import absolute_import, print_function
2 2 from mercurial import (
3 3 minirst,
4 4 )
5 5 from mercurial.utils import (
6 6 stringutil,
7 7 )
8 8
9 9 def debugformat(text, form, **kwargs):
10 blocks, pruned = minirst.parse(text, **kwargs)
10 11 if form == b'html':
11 12 print("html format:")
12 13 out = minirst.format(text, style=form, **kwargs)
13 14 else:
14 15 print("%d column format:" % form)
15 16 out = minirst.format(text, width=form, **kwargs)
16 17
17 18 print("-" * 70)
18 if type(out) == tuple:
19 print(out[0][:-1].decode('utf8'))
19 print(out[:-1].decode('utf8'))
20 if kwargs.get('keep'):
20 21 print("-" * 70)
21 print(stringutil.pprint(out[1]).decode('utf8'))
22 else:
23 print(out[:-1].decode('utf8'))
22 print(stringutil.pprint(pruned).decode('utf8'))
24 23 print("-" * 70)
25 24 print()
26 25
27 26 def debugformats(title, text, **kwargs):
28 27 print("== %s ==" % title)
29 28 debugformat(text, 60, **kwargs)
30 29 debugformat(text, 30, **kwargs)
31 30 debugformat(text, b'html', **kwargs)
32 31
33 32 paragraphs = b"""
34 33 This is some text in the first paragraph.
35 34
36 35 A small indented paragraph.
37 36 It is followed by some lines
38 37 containing random whitespace.
39 38 \n \n \nThe third and final paragraph.
40 39 """
41 40
42 41 debugformats('paragraphs', paragraphs)
43 42
44 43 definitions = b"""
45 44 A Term
46 45 Definition. The indented
47 46 lines make up the definition.
48 47 Another Term
49 48 Another definition. The final line in the
50 49 definition determines the indentation, so
51 50 this will be indented with four spaces.
52 51
53 52 A Nested/Indented Term
54 53 Definition.
55 54 """
56 55
57 56 debugformats('definitions', definitions)
58 57
59 58 literals = br"""
60 59 The fully minimized form is the most
61 60 convenient form::
62 61
63 62 Hello
64 63 literal
65 64 world
66 65
67 66 In the partially minimized form a paragraph
68 67 simply ends with space-double-colon. ::
69 68
70 69 ////////////////////////////////////////
71 70 long un-wrapped line in a literal block
72 71 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
73 72
74 73 ::
75 74
76 75 This literal block is started with '::',
77 76 the so-called expanded form. The paragraph
78 77 with '::' disappears in the final output.
79 78 """
80 79
81 80 debugformats('literals', literals)
82 81
83 82 lists = b"""
84 83 - This is the first list item.
85 84
86 85 Second paragraph in the first list item.
87 86
88 87 - List items need not be separated
89 88 by a blank line.
90 89 - And will be rendered without
91 90 one in any case.
92 91
93 92 We can have indented lists:
94 93
95 94 - This is an indented list item
96 95
97 96 - Another indented list item::
98 97
99 98 - A literal block in the middle
100 99 of an indented list.
101 100
102 101 (The above is not a list item since we are in the literal block.)
103 102
104 103 ::
105 104
106 105 Literal block with no indentation (apart from
107 106 the two spaces added to all literal blocks).
108 107
109 108 1. This is an enumerated list (first item).
110 109 2. Continuing with the second item.
111 110
112 111 (1) foo
113 112 (2) bar
114 113
115 114 1) Another
116 115 2) List
117 116
118 117 Line blocks are also a form of list:
119 118
120 119 | This is the first line.
121 120 The line continues here.
122 121 | This is the second line.
123 122
124 123 Bullet lists are also detected:
125 124
126 125 * This is the first bullet
127 126 * This is the second bullet
128 127 It has 2 lines
129 128 * This is the third bullet
130 129 """
131 130
132 131 debugformats('lists', lists)
133 132
134 133 options = b"""
135 134 There is support for simple option lists,
136 135 but only with long options:
137 136
138 137 -X, --exclude filter an option with a short and long option with an argument
139 138 -I, --include an option with both a short option and a long option
140 139 --all Output all.
141 140 --both Output both (this description is
142 141 quite long).
143 142 --long Output all day long.
144 143
145 144 --par This option has two paragraphs in its description.
146 145 This is the first.
147 146
148 147 This is the second. Blank lines may be omitted between
149 148 options (as above) or left in (as here).
150 149
151 150
152 151 The next paragraph looks like an option list, but lacks the two-space
153 152 marker after the option. It is treated as a normal paragraph:
154 153
155 154 --foo bar baz
156 155 """
157 156
158 157 debugformats('options', options)
159 158
160 159 fields = b"""
161 160 :a: First item.
162 161 :ab: Second item. Indentation and wrapping
163 162 is handled automatically.
164 163
165 164 Next list:
166 165
167 166 :small: The larger key below triggers full indentation here.
168 167 :much too large: This key is big enough to get its own line.
169 168 """
170 169
171 170 debugformats('fields', fields)
172 171
173 172 containers = b"""
174 173 Normal output.
175 174
176 175 .. container:: debug
177 176
178 177 Initial debug output.
179 178
180 179 .. container:: verbose
181 180
182 181 Verbose output.
183 182
184 183 .. container:: debug
185 184
186 185 Debug output.
187 186 """
188 187
189 188 debugformats('containers (normal)', containers)
190 189 debugformats('containers (verbose)', containers, keep=[b'verbose'])
191 190 debugformats('containers (debug)', containers, keep=[b'debug'])
192 191 debugformats('containers (verbose debug)', containers,
193 192 keep=[b'verbose', b'debug'])
194 193
195 194 roles = b"""Please see :hg:`add`."""
196 195 debugformats('roles', roles)
197 196
198 197
199 198 sections = b"""
200 199 Title
201 200 =====
202 201
203 202 Section
204 203 -------
205 204
206 205 Subsection
207 206 ''''''''''
208 207
209 208 Markup: ``foo`` and :hg:`help`
210 209 ------------------------------
211 210 """
212 211 debugformats('sections', sections)
213 212
214 213
215 214 admonitions = b"""
216 215 .. note::
217 216
218 217 This is a note
219 218
220 219 - Bullet 1
221 220 - Bullet 2
222 221
223 222 .. warning:: This is a warning Second
224 223 input line of warning
225 224
226 225 .. danger::
227 226 This is danger
228 227 """
229 228
230 229 debugformats('admonitions', admonitions)
231 230
232 231 comments = b"""
233 232 Some text.
234 233
235 234 .. A comment
236 235
237 236 .. An indented comment
238 237
239 238 Some indented text.
240 239
241 240 ..
242 241
243 242 Empty comment above
244 243 """
245 244
246 245 debugformats('comments', comments)
247 246
248 247
249 248 data = [[b'a', b'b', b'c'],
250 249 [b'1', b'2', b'3'],
251 250 [b'foo', b'bar', b'baz this list is very very very long man']]
252 251
253 252 rst = minirst.maketable(data, 2, True)
254 253 table = b''.join(rst)
255 254
256 255 print(table.decode('utf8'))
257 256
258 257 debugformats('table', table)
259 258
260 259 data = [[b's', b'long', b'line\ngoes on here'],
261 260 [b'', b'xy', b'tried to fix here\n by indenting']]
262 261
263 262 rst = minirst.maketable(data, 1, False)
264 263 table = b''.join(rst)
265 264
266 265 print(table.decode('utf8'))
267 266
268 267 debugformats('table+nl', table)
General Comments 0
You need to be logged in to leave comments. Login now