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