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