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