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