##// END OF EJS Templates
minirst: don't choke on empty text
Matt Mackall -
r15123:9b41ccb2 default
parent child Browse files
Show More
@@ -1,561 +1,562 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 for f, t in substs:
26 for f, t in substs:
27 text = text.replace(f, t)
27 text = text.replace(f, t)
28 return text
28 return text
29
29
30 _blockre = re.compile(r"\n(?:\s*\n)+")
30 _blockre = re.compile(r"\n(?:\s*\n)+")
31
31
32 def findblocks(text):
32 def findblocks(text):
33 """Find continuous blocks of lines in text.
33 """Find continuous blocks of lines in text.
34
34
35 Returns a list of dictionaries representing the blocks. Each block
35 Returns a list of dictionaries representing the blocks. Each block
36 has an 'indent' field and a 'lines' field.
36 has an 'indent' field and a 'lines' field.
37 """
37 """
38 blocks = []
38 blocks = []
39 for b in _blockre.split(text.lstrip('\n').rstrip()):
39 for b in _blockre.split(text.lstrip('\n').rstrip()):
40 lines = b.splitlines()
40 lines = b.splitlines()
41 if lines:
41 indent = min((len(l) - len(l.lstrip())) for l in lines)
42 indent = min((len(l) - len(l.lstrip())) for l in lines)
42 lines = [l[indent:] for l in lines]
43 lines = [l[indent:] for l in lines]
43 blocks.append(dict(indent=indent, lines=lines))
44 blocks.append(dict(indent=indent, lines=lines))
44 return blocks
45 return blocks
45
46
46 def findliteralblocks(blocks):
47 def findliteralblocks(blocks):
47 """Finds literal blocks and adds a 'type' field to the blocks.
48 """Finds literal blocks and adds a 'type' field to the blocks.
48
49
49 Literal blocks are given the type 'literal', all other blocks are
50 Literal blocks are given the type 'literal', all other blocks are
50 given type the 'paragraph'.
51 given type the 'paragraph'.
51 """
52 """
52 i = 0
53 i = 0
53 while i < len(blocks):
54 while i < len(blocks):
54 # Searching for a block that looks like this:
55 # Searching for a block that looks like this:
55 #
56 #
56 # +------------------------------+
57 # +------------------------------+
57 # | paragraph |
58 # | paragraph |
58 # | (ends with "::") |
59 # | (ends with "::") |
59 # +------------------------------+
60 # +------------------------------+
60 # +---------------------------+
61 # +---------------------------+
61 # | indented literal block |
62 # | indented literal block |
62 # +---------------------------+
63 # +---------------------------+
63 blocks[i]['type'] = 'paragraph'
64 blocks[i]['type'] = 'paragraph'
64 if blocks[i]['lines'][-1].endswith('::') and i + 1 < len(blocks):
65 if blocks[i]['lines'][-1].endswith('::') and i + 1 < len(blocks):
65 indent = blocks[i]['indent']
66 indent = blocks[i]['indent']
66 adjustment = blocks[i + 1]['indent'] - indent
67 adjustment = blocks[i + 1]['indent'] - indent
67
68
68 if blocks[i]['lines'] == ['::']:
69 if blocks[i]['lines'] == ['::']:
69 # Expanded form: remove block
70 # Expanded form: remove block
70 del blocks[i]
71 del blocks[i]
71 i -= 1
72 i -= 1
72 elif blocks[i]['lines'][-1].endswith(' ::'):
73 elif blocks[i]['lines'][-1].endswith(' ::'):
73 # Partially minimized form: remove space and both
74 # Partially minimized form: remove space and both
74 # colons.
75 # colons.
75 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-3]
76 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-3]
76 else:
77 else:
77 # Fully minimized form: remove just one colon.
78 # Fully minimized form: remove just one colon.
78 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-1]
79 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-1]
79
80
80 # List items are formatted with a hanging indent. We must
81 # List items are formatted with a hanging indent. We must
81 # correct for this here while we still have the original
82 # correct for this here while we still have the original
82 # information on the indentation of the subsequent literal
83 # information on the indentation of the subsequent literal
83 # blocks available.
84 # blocks available.
84 m = _bulletre.match(blocks[i]['lines'][0])
85 m = _bulletre.match(blocks[i]['lines'][0])
85 if m:
86 if m:
86 indent += m.end()
87 indent += m.end()
87 adjustment -= m.end()
88 adjustment -= m.end()
88
89
89 # Mark the following indented blocks.
90 # Mark the following indented blocks.
90 while i + 1 < len(blocks) and blocks[i + 1]['indent'] > indent:
91 while i + 1 < len(blocks) and blocks[i + 1]['indent'] > indent:
91 blocks[i + 1]['type'] = 'literal'
92 blocks[i + 1]['type'] = 'literal'
92 blocks[i + 1]['indent'] -= adjustment
93 blocks[i + 1]['indent'] -= adjustment
93 i += 1
94 i += 1
94 i += 1
95 i += 1
95 return blocks
96 return blocks
96
97
97 _bulletre = re.compile(r'(-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ')
98 _bulletre = re.compile(r'(-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ')
98 _optionre = re.compile(r'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)'
99 _optionre = re.compile(r'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)'
99 r'((.*) +)(.*)$')
100 r'((.*) +)(.*)$')
100 _fieldre = re.compile(r':(?![: ])([^:]*)(?<! ):[ ]+(.*)')
101 _fieldre = re.compile(r':(?![: ])([^:]*)(?<! ):[ ]+(.*)')
101 _definitionre = re.compile(r'[^ ]')
102 _definitionre = re.compile(r'[^ ]')
102 _tablere = re.compile(r'(=+\s+)*=+')
103 _tablere = re.compile(r'(=+\s+)*=+')
103
104
104 def splitparagraphs(blocks):
105 def splitparagraphs(blocks):
105 """Split paragraphs into lists."""
106 """Split paragraphs into lists."""
106 # Tuples with (list type, item regexp, single line items?). Order
107 # Tuples with (list type, item regexp, single line items?). Order
107 # matters: definition lists has the least specific regexp and must
108 # matters: definition lists has the least specific regexp and must
108 # come last.
109 # come last.
109 listtypes = [('bullet', _bulletre, True),
110 listtypes = [('bullet', _bulletre, True),
110 ('option', _optionre, True),
111 ('option', _optionre, True),
111 ('field', _fieldre, True),
112 ('field', _fieldre, True),
112 ('definition', _definitionre, False)]
113 ('definition', _definitionre, False)]
113
114
114 def match(lines, i, itemre, singleline):
115 def match(lines, i, itemre, singleline):
115 """Does itemre match an item at line i?
116 """Does itemre match an item at line i?
116
117
117 A list item can be followed by an idented line or another list
118 A list item can be followed by an idented line or another list
118 item (but only if singleline is True).
119 item (but only if singleline is True).
119 """
120 """
120 line1 = lines[i]
121 line1 = lines[i]
121 line2 = i + 1 < len(lines) and lines[i + 1] or ''
122 line2 = i + 1 < len(lines) and lines[i + 1] or ''
122 if not itemre.match(line1):
123 if not itemre.match(line1):
123 return False
124 return False
124 if singleline:
125 if singleline:
125 return line2 == '' or line2[0] == ' ' or itemre.match(line2)
126 return line2 == '' or line2[0] == ' ' or itemre.match(line2)
126 else:
127 else:
127 return line2.startswith(' ')
128 return line2.startswith(' ')
128
129
129 i = 0
130 i = 0
130 while i < len(blocks):
131 while i < len(blocks):
131 if blocks[i]['type'] == 'paragraph':
132 if blocks[i]['type'] == 'paragraph':
132 lines = blocks[i]['lines']
133 lines = blocks[i]['lines']
133 for type, itemre, singleline in listtypes:
134 for type, itemre, singleline in listtypes:
134 if match(lines, 0, itemre, singleline):
135 if match(lines, 0, itemre, singleline):
135 items = []
136 items = []
136 for j, line in enumerate(lines):
137 for j, line in enumerate(lines):
137 if match(lines, j, itemre, singleline):
138 if match(lines, j, itemre, singleline):
138 items.append(dict(type=type, lines=[],
139 items.append(dict(type=type, lines=[],
139 indent=blocks[i]['indent']))
140 indent=blocks[i]['indent']))
140 items[-1]['lines'].append(line)
141 items[-1]['lines'].append(line)
141 blocks[i:i + 1] = items
142 blocks[i:i + 1] = items
142 break
143 break
143 i += 1
144 i += 1
144 return blocks
145 return blocks
145
146
146 _fieldwidth = 12
147 _fieldwidth = 12
147
148
148 def updatefieldlists(blocks):
149 def updatefieldlists(blocks):
149 """Find key and maximum key width for field lists."""
150 """Find key and maximum key width for field lists."""
150 i = 0
151 i = 0
151 while i < len(blocks):
152 while i < len(blocks):
152 if blocks[i]['type'] != 'field':
153 if blocks[i]['type'] != 'field':
153 i += 1
154 i += 1
154 continue
155 continue
155
156
156 keywidth = 0
157 keywidth = 0
157 j = i
158 j = i
158 while j < len(blocks) and blocks[j]['type'] == 'field':
159 while j < len(blocks) and blocks[j]['type'] == 'field':
159 m = _fieldre.match(blocks[j]['lines'][0])
160 m = _fieldre.match(blocks[j]['lines'][0])
160 key, rest = m.groups()
161 key, rest = m.groups()
161 blocks[j]['lines'][0] = rest
162 blocks[j]['lines'][0] = rest
162 blocks[j]['key'] = key
163 blocks[j]['key'] = key
163 keywidth = max(keywidth, len(key))
164 keywidth = max(keywidth, len(key))
164 j += 1
165 j += 1
165
166
166 for block in blocks[i:j]:
167 for block in blocks[i:j]:
167 block['keywidth'] = keywidth
168 block['keywidth'] = keywidth
168 i = j + 1
169 i = j + 1
169
170
170 return blocks
171 return blocks
171
172
172 def updateoptionlists(blocks):
173 def updateoptionlists(blocks):
173 i = 0
174 i = 0
174 while i < len(blocks):
175 while i < len(blocks):
175 if blocks[i]['type'] != 'option':
176 if blocks[i]['type'] != 'option':
176 i += 1
177 i += 1
177 continue
178 continue
178
179
179 optstrwidth = 0
180 optstrwidth = 0
180 j = i
181 j = i
181 while j < len(blocks) and blocks[j]['type'] == 'option':
182 while j < len(blocks) and blocks[j]['type'] == 'option':
182 m = _optionre.match(blocks[j]['lines'][0])
183 m = _optionre.match(blocks[j]['lines'][0])
183
184
184 shortoption = m.group(2)
185 shortoption = m.group(2)
185 group3 = m.group(3)
186 group3 = m.group(3)
186 longoption = group3[2:].strip()
187 longoption = group3[2:].strip()
187 desc = m.group(6).strip()
188 desc = m.group(6).strip()
188 longoptionarg = m.group(5).strip()
189 longoptionarg = m.group(5).strip()
189 blocks[j]['lines'][0] = desc
190 blocks[j]['lines'][0] = desc
190
191
191 noshortop = ''
192 noshortop = ''
192 if not shortoption:
193 if not shortoption:
193 noshortop = ' '
194 noshortop = ' '
194
195
195 opt = "%s%s" % (shortoption and "-%s " % shortoption or '',
196 opt = "%s%s" % (shortoption and "-%s " % shortoption or '',
196 ("%s--%s %s") % (noshortop, longoption,
197 ("%s--%s %s") % (noshortop, longoption,
197 longoptionarg))
198 longoptionarg))
198 opt = opt.rstrip()
199 opt = opt.rstrip()
199 blocks[j]['optstr'] = opt
200 blocks[j]['optstr'] = opt
200 optstrwidth = max(optstrwidth, encoding.colwidth(opt))
201 optstrwidth = max(optstrwidth, encoding.colwidth(opt))
201 j += 1
202 j += 1
202
203
203 for block in blocks[i:j]:
204 for block in blocks[i:j]:
204 block['optstrwidth'] = optstrwidth
205 block['optstrwidth'] = optstrwidth
205 i = j + 1
206 i = j + 1
206 return blocks
207 return blocks
207
208
208 def prunecontainers(blocks, keep):
209 def prunecontainers(blocks, keep):
209 """Prune unwanted containers.
210 """Prune unwanted containers.
210
211
211 The blocks must have a 'type' field, i.e., they should have been
212 The blocks must have a 'type' field, i.e., they should have been
212 run through findliteralblocks first.
213 run through findliteralblocks first.
213 """
214 """
214 pruned = []
215 pruned = []
215 i = 0
216 i = 0
216 while i + 1 < len(blocks):
217 while i + 1 < len(blocks):
217 # Searching for a block that looks like this:
218 # Searching for a block that looks like this:
218 #
219 #
219 # +-------+---------------------------+
220 # +-------+---------------------------+
220 # | ".. container ::" type |
221 # | ".. container ::" type |
221 # +---+ |
222 # +---+ |
222 # | blocks |
223 # | blocks |
223 # +-------------------------------+
224 # +-------------------------------+
224 if (blocks[i]['type'] == 'paragraph' and
225 if (blocks[i]['type'] == 'paragraph' and
225 blocks[i]['lines'][0].startswith('.. container::')):
226 blocks[i]['lines'][0].startswith('.. container::')):
226 indent = blocks[i]['indent']
227 indent = blocks[i]['indent']
227 adjustment = blocks[i + 1]['indent'] - indent
228 adjustment = blocks[i + 1]['indent'] - indent
228 containertype = blocks[i]['lines'][0][15:]
229 containertype = blocks[i]['lines'][0][15:]
229 prune = containertype not in keep
230 prune = containertype not in keep
230 if prune:
231 if prune:
231 pruned.append(containertype)
232 pruned.append(containertype)
232
233
233 # Always delete "..container:: type" block
234 # Always delete "..container:: type" block
234 del blocks[i]
235 del blocks[i]
235 j = i
236 j = i
236 i -= 1
237 i -= 1
237 while j < len(blocks) and blocks[j]['indent'] > indent:
238 while j < len(blocks) and blocks[j]['indent'] > indent:
238 if prune:
239 if prune:
239 del blocks[j]
240 del blocks[j]
240 else:
241 else:
241 blocks[j]['indent'] -= adjustment
242 blocks[j]['indent'] -= adjustment
242 j += 1
243 j += 1
243 i += 1
244 i += 1
244 return blocks, pruned
245 return blocks, pruned
245
246
246 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
247 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
247
248
248 def findtables(blocks):
249 def findtables(blocks):
249 '''Find simple tables
250 '''Find simple tables
250
251
251 Only simple one-line table elements are supported
252 Only simple one-line table elements are supported
252 '''
253 '''
253
254
254 for block in blocks:
255 for block in blocks:
255 # Searching for a block that looks like this:
256 # Searching for a block that looks like this:
256 #
257 #
257 # === ==== ===
258 # === ==== ===
258 # A B C
259 # A B C
259 # === ==== === <- optional
260 # === ==== === <- optional
260 # 1 2 3
261 # 1 2 3
261 # x y z
262 # x y z
262 # === ==== ===
263 # === ==== ===
263 if (block['type'] == 'paragraph' and
264 if (block['type'] == 'paragraph' and
264 len(block['lines']) > 4 and
265 len(block['lines']) > 4 and
265 _tablere.match(block['lines'][0]) and
266 _tablere.match(block['lines'][0]) and
266 block['lines'][0] == block['lines'][-1]):
267 block['lines'][0] == block['lines'][-1]):
267 block['type'] = 'table'
268 block['type'] = 'table'
268 block['header'] = False
269 block['header'] = False
269 div = block['lines'][0]
270 div = block['lines'][0]
270 columns = [x for x in xrange(len(div))
271 columns = [x for x in xrange(len(div))
271 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
272 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
272 rows = []
273 rows = []
273 for l in block['lines'][1:-1]:
274 for l in block['lines'][1:-1]:
274 if l == div:
275 if l == div:
275 block['header'] = True
276 block['header'] = True
276 continue
277 continue
277 row = []
278 row = []
278 for n, start in enumerate(columns):
279 for n, start in enumerate(columns):
279 if n + 1 < len(columns):
280 if n + 1 < len(columns):
280 row.append(l[start:columns[n + 1]].strip())
281 row.append(l[start:columns[n + 1]].strip())
281 else:
282 else:
282 row.append(l[start:].strip())
283 row.append(l[start:].strip())
283 rows.append(row)
284 rows.append(row)
284 block['table'] = rows
285 block['table'] = rows
285
286
286 return blocks
287 return blocks
287
288
288 def findsections(blocks):
289 def findsections(blocks):
289 """Finds sections.
290 """Finds sections.
290
291
291 The blocks must have a 'type' field, i.e., they should have been
292 The blocks must have a 'type' field, i.e., they should have been
292 run through findliteralblocks first.
293 run through findliteralblocks first.
293 """
294 """
294 for block in blocks:
295 for block in blocks:
295 # Searching for a block that looks like this:
296 # Searching for a block that looks like this:
296 #
297 #
297 # +------------------------------+
298 # +------------------------------+
298 # | Section title |
299 # | Section title |
299 # | ------------- |
300 # | ------------- |
300 # +------------------------------+
301 # +------------------------------+
301 if (block['type'] == 'paragraph' and
302 if (block['type'] == 'paragraph' and
302 len(block['lines']) == 2 and
303 len(block['lines']) == 2 and
303 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
304 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
304 _sectionre.match(block['lines'][1])):
305 _sectionre.match(block['lines'][1])):
305 block['underline'] = block['lines'][1][0]
306 block['underline'] = block['lines'][1][0]
306 block['type'] = 'section'
307 block['type'] = 'section'
307 del block['lines'][1]
308 del block['lines'][1]
308 return blocks
309 return blocks
309
310
310 def inlineliterals(blocks):
311 def inlineliterals(blocks):
311 substs = [('``', '"')]
312 substs = [('``', '"')]
312 for b in blocks:
313 for b in blocks:
313 if b['type'] in ('paragraph', 'section'):
314 if b['type'] in ('paragraph', 'section'):
314 b['lines'] = [replace(l, substs) for l in b['lines']]
315 b['lines'] = [replace(l, substs) for l in b['lines']]
315 return blocks
316 return blocks
316
317
317 def hgrole(blocks):
318 def hgrole(blocks):
318 substs = [(':hg:`', '"hg '), ('`', '"')]
319 substs = [(':hg:`', '"hg '), ('`', '"')]
319 for b in blocks:
320 for b in blocks:
320 if b['type'] in ('paragraph', 'section'):
321 if b['type'] in ('paragraph', 'section'):
321 # Turn :hg:`command` into "hg command". This also works
322 # Turn :hg:`command` into "hg command". This also works
322 # when there is a line break in the command and relies on
323 # when there is a line break in the command and relies on
323 # the fact that we have no stray back-quotes in the input
324 # the fact that we have no stray back-quotes in the input
324 # (run the blocks through inlineliterals first).
325 # (run the blocks through inlineliterals first).
325 b['lines'] = [replace(l, substs) for l in b['lines']]
326 b['lines'] = [replace(l, substs) for l in b['lines']]
326 return blocks
327 return blocks
327
328
328 def addmargins(blocks):
329 def addmargins(blocks):
329 """Adds empty blocks for vertical spacing.
330 """Adds empty blocks for vertical spacing.
330
331
331 This groups bullets, options, and definitions together with no vertical
332 This groups bullets, options, and definitions together with no vertical
332 space between them, and adds an empty block between all other blocks.
333 space between them, and adds an empty block between all other blocks.
333 """
334 """
334 i = 1
335 i = 1
335 while i < len(blocks):
336 while i < len(blocks):
336 if (blocks[i]['type'] == blocks[i - 1]['type'] and
337 if (blocks[i]['type'] == blocks[i - 1]['type'] and
337 blocks[i]['type'] in ('bullet', 'option', 'field')):
338 blocks[i]['type'] in ('bullet', 'option', 'field')):
338 i += 1
339 i += 1
339 else:
340 else:
340 blocks.insert(i, dict(lines=[''], indent=0, type='margin'))
341 blocks.insert(i, dict(lines=[''], indent=0, type='margin'))
341 i += 2
342 i += 2
342 return blocks
343 return blocks
343
344
344 def prunecomments(blocks):
345 def prunecomments(blocks):
345 """Remove comments."""
346 """Remove comments."""
346 i = 0
347 i = 0
347 while i < len(blocks):
348 while i < len(blocks):
348 b = blocks[i]
349 b = blocks[i]
349 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
350 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
350 b['lines'] == ['..']):
351 b['lines'] == ['..']):
351 del blocks[i]
352 del blocks[i]
352 if i < len(blocks) and blocks[i]['type'] == 'margin':
353 if i < len(blocks) and blocks[i]['type'] == 'margin':
353 del blocks[i]
354 del blocks[i]
354 else:
355 else:
355 i += 1
356 i += 1
356 return blocks
357 return blocks
357
358
358 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
359 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
359 r"error|hint|important|note|tip|warning)::",
360 r"error|hint|important|note|tip|warning)::",
360 flags=re.IGNORECASE)
361 flags=re.IGNORECASE)
361
362
362 def findadmonitions(blocks):
363 def findadmonitions(blocks):
363 """
364 """
364 Makes the type of the block an admonition block if
365 Makes the type of the block an admonition block if
365 the first line is an admonition directive
366 the first line is an admonition directive
366 """
367 """
367 i = 0
368 i = 0
368 while i < len(blocks):
369 while i < len(blocks):
369 m = _admonitionre.match(blocks[i]['lines'][0])
370 m = _admonitionre.match(blocks[i]['lines'][0])
370 if m:
371 if m:
371 blocks[i]['type'] = 'admonition'
372 blocks[i]['type'] = 'admonition'
372 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
373 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
373
374
374 firstline = blocks[i]['lines'][0][m.end() + 1:]
375 firstline = blocks[i]['lines'][0][m.end() + 1:]
375 if firstline:
376 if firstline:
376 blocks[i]['lines'].insert(1, ' ' + firstline)
377 blocks[i]['lines'].insert(1, ' ' + firstline)
377
378
378 blocks[i]['admonitiontitle'] = admonitiontitle
379 blocks[i]['admonitiontitle'] = admonitiontitle
379 del blocks[i]['lines'][0]
380 del blocks[i]['lines'][0]
380 i = i + 1
381 i = i + 1
381 return blocks
382 return blocks
382
383
383 _admonitiontitles = {'attention': _('Attention:'),
384 _admonitiontitles = {'attention': _('Attention:'),
384 'caution': _('Caution:'),
385 'caution': _('Caution:'),
385 'danger': _('!Danger!') ,
386 'danger': _('!Danger!') ,
386 'error': _('Error:'),
387 'error': _('Error:'),
387 'hint': _('Hint:'),
388 'hint': _('Hint:'),
388 'important': _('Important:'),
389 'important': _('Important:'),
389 'note': _('Note:'),
390 'note': _('Note:'),
390 'tip': _('Tip:'),
391 'tip': _('Tip:'),
391 'warning': _('Warning!')}
392 'warning': _('Warning!')}
392
393
393 def formatoption(block, width):
394 def formatoption(block, width):
394 desc = ' '.join(map(str.strip, block['lines']))
395 desc = ' '.join(map(str.strip, block['lines']))
395 colwidth = encoding.colwidth(block['optstr'])
396 colwidth = encoding.colwidth(block['optstr'])
396 usablewidth = width - 1
397 usablewidth = width - 1
397 hanging = block['optstrwidth']
398 hanging = block['optstrwidth']
398 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
399 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
399 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
400 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
400 return ' %s' % (util.wrap(desc, usablewidth,
401 return ' %s' % (util.wrap(desc, usablewidth,
401 initindent=initindent,
402 initindent=initindent,
402 hangindent=hangindent))
403 hangindent=hangindent))
403
404
404 def formatblock(block, width):
405 def formatblock(block, width):
405 """Format a block according to width."""
406 """Format a block according to width."""
406 if width <= 0:
407 if width <= 0:
407 width = 78
408 width = 78
408 indent = ' ' * block['indent']
409 indent = ' ' * block['indent']
409 if block['type'] == 'admonition':
410 if block['type'] == 'admonition':
410 admonition = _admonitiontitles[block['admonitiontitle']]
411 admonition = _admonitiontitles[block['admonitiontitle']]
411 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
412 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
412
413
413 defindent = indent + hang * ' '
414 defindent = indent + hang * ' '
414 text = ' '.join(map(str.strip, block['lines']))
415 text = ' '.join(map(str.strip, block['lines']))
415 return '%s\n%s' % (indent + admonition, util.wrap(text, width=width,
416 return '%s\n%s' % (indent + admonition, util.wrap(text, width=width,
416 initindent=defindent,
417 initindent=defindent,
417 hangindent=defindent))
418 hangindent=defindent))
418 if block['type'] == 'margin':
419 if block['type'] == 'margin':
419 return ''
420 return ''
420 if block['type'] == 'literal':
421 if block['type'] == 'literal':
421 indent += ' '
422 indent += ' '
422 return indent + ('\n' + indent).join(block['lines'])
423 return indent + ('\n' + indent).join(block['lines'])
423 if block['type'] == 'section':
424 if block['type'] == 'section':
424 underline = encoding.colwidth(block['lines'][0]) * block['underline']
425 underline = encoding.colwidth(block['lines'][0]) * block['underline']
425 return "%s%s\n%s%s" % (indent, block['lines'][0],indent, underline)
426 return "%s%s\n%s%s" % (indent, block['lines'][0],indent, underline)
426 if block['type'] == 'table':
427 if block['type'] == 'table':
427 table = block['table']
428 table = block['table']
428 # compute column widths
429 # compute column widths
429 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
430 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
430 text = ''
431 text = ''
431 span = sum(widths) + len(widths) - 1
432 span = sum(widths) + len(widths) - 1
432 indent = ' ' * block['indent']
433 indent = ' ' * block['indent']
433 hang = ' ' * (len(indent) + span - widths[-1])
434 hang = ' ' * (len(indent) + span - widths[-1])
434 f = ' '.join('%%-%ds' % n for n in widths)
435 f = ' '.join('%%-%ds' % n for n in widths)
435
436
436 for row in table:
437 for row in table:
437 l = f % tuple(row)
438 l = f % tuple(row)
438 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
439 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
439 if not text and block['header']:
440 if not text and block['header']:
440 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
441 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
441 else:
442 else:
442 text += l + "\n"
443 text += l + "\n"
443 return text
444 return text
444 if block['type'] == 'definition':
445 if block['type'] == 'definition':
445 term = indent + block['lines'][0]
446 term = indent + block['lines'][0]
446 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
447 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
447 defindent = indent + hang * ' '
448 defindent = indent + hang * ' '
448 text = ' '.join(map(str.strip, block['lines'][1:]))
449 text = ' '.join(map(str.strip, block['lines'][1:]))
449 return '%s\n%s' % (term, util.wrap(text, width=width,
450 return '%s\n%s' % (term, util.wrap(text, width=width,
450 initindent=defindent,
451 initindent=defindent,
451 hangindent=defindent))
452 hangindent=defindent))
452 subindent = indent
453 subindent = indent
453 if block['type'] == 'bullet':
454 if block['type'] == 'bullet':
454 if block['lines'][0].startswith('| '):
455 if block['lines'][0].startswith('| '):
455 # Remove bullet for line blocks and add no extra
456 # Remove bullet for line blocks and add no extra
456 # indention.
457 # indention.
457 block['lines'][0] = block['lines'][0][2:]
458 block['lines'][0] = block['lines'][0][2:]
458 else:
459 else:
459 m = _bulletre.match(block['lines'][0])
460 m = _bulletre.match(block['lines'][0])
460 subindent = indent + m.end() * ' '
461 subindent = indent + m.end() * ' '
461 elif block['type'] == 'field':
462 elif block['type'] == 'field':
462 keywidth = block['keywidth']
463 keywidth = block['keywidth']
463 key = block['key']
464 key = block['key']
464
465
465 subindent = indent + _fieldwidth * ' '
466 subindent = indent + _fieldwidth * ' '
466 if len(key) + 2 > _fieldwidth:
467 if len(key) + 2 > _fieldwidth:
467 # key too large, use full line width
468 # key too large, use full line width
468 key = key.ljust(width)
469 key = key.ljust(width)
469 elif keywidth + 2 < _fieldwidth:
470 elif keywidth + 2 < _fieldwidth:
470 # all keys are small, add only two spaces
471 # all keys are small, add only two spaces
471 key = key.ljust(keywidth + 2)
472 key = key.ljust(keywidth + 2)
472 subindent = indent + (keywidth + 2) * ' '
473 subindent = indent + (keywidth + 2) * ' '
473 else:
474 else:
474 # mixed sizes, use fieldwidth for this one
475 # mixed sizes, use fieldwidth for this one
475 key = key.ljust(_fieldwidth)
476 key = key.ljust(_fieldwidth)
476 block['lines'][0] = key + block['lines'][0]
477 block['lines'][0] = key + block['lines'][0]
477 elif block['type'] == 'option':
478 elif block['type'] == 'option':
478 return formatoption(block, width)
479 return formatoption(block, width)
479
480
480 text = ' '.join(map(str.strip, block['lines']))
481 text = ' '.join(map(str.strip, block['lines']))
481 return util.wrap(text, width=width,
482 return util.wrap(text, width=width,
482 initindent=indent,
483 initindent=indent,
483 hangindent=subindent)
484 hangindent=subindent)
484
485
485 def parse(text, indent=0, keep=None):
486 def parse(text, indent=0, keep=None):
486 """Parse text into a list of blocks"""
487 """Parse text into a list of blocks"""
487 pruned = []
488 pruned = []
488 blocks = findblocks(text)
489 blocks = findblocks(text)
489 for b in blocks:
490 for b in blocks:
490 b['indent'] += indent
491 b['indent'] += indent
491 blocks = findliteralblocks(blocks)
492 blocks = findliteralblocks(blocks)
492 blocks = findtables(blocks)
493 blocks = findtables(blocks)
493 blocks, pruned = prunecontainers(blocks, keep or [])
494 blocks, pruned = prunecontainers(blocks, keep or [])
494 blocks = findsections(blocks)
495 blocks = findsections(blocks)
495 blocks = inlineliterals(blocks)
496 blocks = inlineliterals(blocks)
496 blocks = hgrole(blocks)
497 blocks = hgrole(blocks)
497 blocks = splitparagraphs(blocks)
498 blocks = splitparagraphs(blocks)
498 blocks = updatefieldlists(blocks)
499 blocks = updatefieldlists(blocks)
499 blocks = updateoptionlists(blocks)
500 blocks = updateoptionlists(blocks)
500 blocks = addmargins(blocks)
501 blocks = addmargins(blocks)
501 blocks = prunecomments(blocks)
502 blocks = prunecomments(blocks)
502 blocks = findadmonitions(blocks)
503 blocks = findadmonitions(blocks)
503 return blocks, pruned
504 return blocks, pruned
504
505
505 def formatblocks(blocks, width):
506 def formatblocks(blocks, width):
506 text = '\n'.join(formatblock(b, width) for b in blocks)
507 text = '\n'.join(formatblock(b, width) for b in blocks)
507 return text
508 return text
508
509
509 def format(text, width, indent=0, keep=None):
510 def format(text, width, indent=0, keep=None):
510 """Parse and format the text according to width."""
511 """Parse and format the text according to width."""
511 blocks, pruned = parse(text, indent, keep or [])
512 blocks, pruned = parse(text, indent, keep or [])
512 text = '\n'.join(formatblock(b, width) for b in blocks)
513 text = '\n'.join(formatblock(b, width) for b in blocks)
513 if keep is None:
514 if keep is None:
514 return text
515 return text
515 else:
516 else:
516 return text, pruned
517 return text, pruned
517
518
518 def getsections(blocks):
519 def getsections(blocks):
519 '''return a list of (section name, nesting level, blocks) tuples'''
520 '''return a list of (section name, nesting level, blocks) tuples'''
520 nest = ""
521 nest = ""
521 level = 0
522 level = 0
522 secs = []
523 secs = []
523 for b in blocks:
524 for b in blocks:
524 if b['type'] == 'section':
525 if b['type'] == 'section':
525 i = b['underline']
526 i = b['underline']
526 if i not in nest:
527 if i not in nest:
527 nest += i
528 nest += i
528 level = nest.index(i) + 1
529 level = nest.index(i) + 1
529 nest = nest[:level]
530 nest = nest[:level]
530 secs.append((b['lines'][0], level, [b]))
531 secs.append((b['lines'][0], level, [b]))
531 else:
532 else:
532 if not secs:
533 if not secs:
533 # add an initial empty section
534 # add an initial empty section
534 secs = [('', 0, [])]
535 secs = [('', 0, [])]
535 secs[-1][2].append(b)
536 secs[-1][2].append(b)
536 return secs
537 return secs
537
538
538 def decorateblocks(blocks, width):
539 def decorateblocks(blocks, width):
539 '''generate a list of (section name, line text) pairs for search'''
540 '''generate a list of (section name, line text) pairs for search'''
540 lines = []
541 lines = []
541 for s in getsections(blocks):
542 for s in getsections(blocks):
542 section = s[0]
543 section = s[0]
543 text = formatblocks(s[2], width)
544 text = formatblocks(s[2], width)
544 lines.append([(section, l) for l in text.splitlines(True)])
545 lines.append([(section, l) for l in text.splitlines(True)])
545 return lines
546 return lines
546
547
547 def maketable(data, indent=0, header=False):
548 def maketable(data, indent=0, header=False):
548 '''Generate an RST table for the given table data'''
549 '''Generate an RST table for the given table data'''
549
550
550 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
551 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
551 indent = ' ' * indent
552 indent = ' ' * indent
552 f = indent + ' '.join('%%-%ds' % w for w in widths) + '\n'
553 f = indent + ' '.join('%%-%ds' % w for w in widths) + '\n'
553 div = indent + ' '.join('=' * w for w in widths) + '\n'
554 div = indent + ' '.join('=' * w for w in widths) + '\n'
554
555
555 out = [div]
556 out = [div]
556 for row in data:
557 for row in data:
557 out.append(f % tuple(row))
558 out.append(f % tuple(row))
558 if header and len(data) > 1:
559 if header and len(data) > 1:
559 out.insert(2, div)
560 out.insert(2, div)
560 out.append(div)
561 out.append(div)
561 return ''.join(out)
562 return ''.join(out)
General Comments 0
You need to be logged in to leave comments. Login now