##// END OF EJS Templates
minirst: find admonitions before pruning comments and adding margins...
Simon Heimberg -
r19994:fc251b1a stable
parent child Browse files
Show More
@@ -1,703 +1,703 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 else:
384 else:
385 blocks.insert(i, dict(lines=[''], indent=0, type='margin'))
385 blocks.insert(i, dict(lines=[''], indent=0, type='margin'))
386 i += 2
386 i += 2
387 return blocks
387 return blocks
388
388
389 def prunecomments(blocks):
389 def prunecomments(blocks):
390 """Remove comments."""
390 """Remove comments."""
391 i = 0
391 i = 0
392 while i < len(blocks):
392 while i < len(blocks):
393 b = blocks[i]
393 b = blocks[i]
394 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
394 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
395 b['lines'] == ['..']):
395 b['lines'] == ['..']):
396 del blocks[i]
396 del blocks[i]
397 if i < len(blocks) and blocks[i]['type'] == 'margin':
397 if i < len(blocks) and blocks[i]['type'] == 'margin':
398 del blocks[i]
398 del blocks[i]
399 else:
399 else:
400 i += 1
400 i += 1
401 return blocks
401 return blocks
402
402
403 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
403 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
404 r"error|hint|important|note|tip|warning)::",
404 r"error|hint|important|note|tip|warning)::",
405 flags=re.IGNORECASE)
405 flags=re.IGNORECASE)
406
406
407 def findadmonitions(blocks):
407 def findadmonitions(blocks):
408 """
408 """
409 Makes the type of the block an admonition block if
409 Makes the type of the block an admonition block if
410 the first line is an admonition directive
410 the first line is an admonition directive
411 """
411 """
412 i = 0
412 i = 0
413 while i < len(blocks):
413 while i < len(blocks):
414 m = _admonitionre.match(blocks[i]['lines'][0])
414 m = _admonitionre.match(blocks[i]['lines'][0])
415 if m:
415 if m:
416 blocks[i]['type'] = 'admonition'
416 blocks[i]['type'] = 'admonition'
417 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
417 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
418
418
419 firstline = blocks[i]['lines'][0][m.end() + 1:]
419 firstline = blocks[i]['lines'][0][m.end() + 1:]
420 if firstline:
420 if firstline:
421 blocks[i]['lines'].insert(1, ' ' + firstline)
421 blocks[i]['lines'].insert(1, ' ' + firstline)
422
422
423 blocks[i]['admonitiontitle'] = admonitiontitle
423 blocks[i]['admonitiontitle'] = admonitiontitle
424 del blocks[i]['lines'][0]
424 del blocks[i]['lines'][0]
425 i = i + 1
425 i = i + 1
426 return blocks
426 return blocks
427
427
428 _admonitiontitles = {'attention': _('Attention:'),
428 _admonitiontitles = {'attention': _('Attention:'),
429 'caution': _('Caution:'),
429 'caution': _('Caution:'),
430 'danger': _('!Danger!') ,
430 'danger': _('!Danger!') ,
431 'error': _('Error:'),
431 'error': _('Error:'),
432 'hint': _('Hint:'),
432 'hint': _('Hint:'),
433 'important': _('Important:'),
433 'important': _('Important:'),
434 'note': _('Note:'),
434 'note': _('Note:'),
435 'tip': _('Tip:'),
435 'tip': _('Tip:'),
436 'warning': _('Warning!')}
436 'warning': _('Warning!')}
437
437
438 def formatoption(block, width):
438 def formatoption(block, width):
439 desc = ' '.join(map(str.strip, block['lines']))
439 desc = ' '.join(map(str.strip, block['lines']))
440 colwidth = encoding.colwidth(block['optstr'])
440 colwidth = encoding.colwidth(block['optstr'])
441 usablewidth = width - 1
441 usablewidth = width - 1
442 hanging = block['optstrwidth']
442 hanging = block['optstrwidth']
443 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
443 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
444 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
444 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
445 return ' %s\n' % (util.wrap(desc, usablewidth,
445 return ' %s\n' % (util.wrap(desc, usablewidth,
446 initindent=initindent,
446 initindent=initindent,
447 hangindent=hangindent))
447 hangindent=hangindent))
448
448
449 def formatblock(block, width):
449 def formatblock(block, width):
450 """Format a block according to width."""
450 """Format a block according to width."""
451 if width <= 0:
451 if width <= 0:
452 width = 78
452 width = 78
453 indent = ' ' * block['indent']
453 indent = ' ' * block['indent']
454 if block['type'] == 'admonition':
454 if block['type'] == 'admonition':
455 admonition = _admonitiontitles[block['admonitiontitle']]
455 admonition = _admonitiontitles[block['admonitiontitle']]
456 if not block['lines']:
456 if not block['lines']:
457 return indent + admonition + '\n'
457 return indent + admonition + '\n'
458 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
458 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
459
459
460 defindent = indent + hang * ' '
460 defindent = indent + hang * ' '
461 text = ' '.join(map(str.strip, block['lines']))
461 text = ' '.join(map(str.strip, block['lines']))
462 return '%s\n%s\n' % (indent + admonition,
462 return '%s\n%s\n' % (indent + admonition,
463 util.wrap(text, width=width,
463 util.wrap(text, width=width,
464 initindent=defindent,
464 initindent=defindent,
465 hangindent=defindent))
465 hangindent=defindent))
466 if block['type'] == 'margin':
466 if block['type'] == 'margin':
467 return '\n'
467 return '\n'
468 if block['type'] == 'literal':
468 if block['type'] == 'literal':
469 indent += ' '
469 indent += ' '
470 return indent + ('\n' + indent).join(block['lines']) + '\n'
470 return indent + ('\n' + indent).join(block['lines']) + '\n'
471 if block['type'] == 'section':
471 if block['type'] == 'section':
472 underline = encoding.colwidth(block['lines'][0]) * block['underline']
472 underline = encoding.colwidth(block['lines'][0]) * block['underline']
473 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
473 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
474 if block['type'] == 'table':
474 if block['type'] == 'table':
475 table = block['table']
475 table = block['table']
476 # compute column widths
476 # compute column widths
477 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
477 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
478 text = ''
478 text = ''
479 span = sum(widths) + len(widths) - 1
479 span = sum(widths) + len(widths) - 1
480 indent = ' ' * block['indent']
480 indent = ' ' * block['indent']
481 hang = ' ' * (len(indent) + span - widths[-1])
481 hang = ' ' * (len(indent) + span - widths[-1])
482
482
483 for row in table:
483 for row in table:
484 l = []
484 l = []
485 for w, v in zip(widths, row):
485 for w, v in zip(widths, row):
486 pad = ' ' * (w - encoding.colwidth(v))
486 pad = ' ' * (w - encoding.colwidth(v))
487 l.append(v + pad)
487 l.append(v + pad)
488 l = ' '.join(l)
488 l = ' '.join(l)
489 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
489 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
490 if not text and block['header']:
490 if not text and block['header']:
491 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
491 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
492 else:
492 else:
493 text += l + "\n"
493 text += l + "\n"
494 return text
494 return text
495 if block['type'] == 'definition':
495 if block['type'] == 'definition':
496 term = indent + block['lines'][0]
496 term = indent + block['lines'][0]
497 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
497 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
498 defindent = indent + hang * ' '
498 defindent = indent + hang * ' '
499 text = ' '.join(map(str.strip, block['lines'][1:]))
499 text = ' '.join(map(str.strip, block['lines'][1:]))
500 return '%s\n%s\n' % (term, util.wrap(text, width=width,
500 return '%s\n%s\n' % (term, util.wrap(text, width=width,
501 initindent=defindent,
501 initindent=defindent,
502 hangindent=defindent))
502 hangindent=defindent))
503 subindent = indent
503 subindent = indent
504 if block['type'] == 'bullet':
504 if block['type'] == 'bullet':
505 if block['lines'][0].startswith('| '):
505 if block['lines'][0].startswith('| '):
506 # Remove bullet for line blocks and add no extra
506 # Remove bullet for line blocks and add no extra
507 # indention.
507 # indention.
508 block['lines'][0] = block['lines'][0][2:]
508 block['lines'][0] = block['lines'][0][2:]
509 else:
509 else:
510 m = _bulletre.match(block['lines'][0])
510 m = _bulletre.match(block['lines'][0])
511 subindent = indent + m.end() * ' '
511 subindent = indent + m.end() * ' '
512 elif block['type'] == 'field':
512 elif block['type'] == 'field':
513 key = block['key']
513 key = block['key']
514 subindent = indent + _fieldwidth * ' '
514 subindent = indent + _fieldwidth * ' '
515 if len(key) + 2 > _fieldwidth:
515 if len(key) + 2 > _fieldwidth:
516 # key too large, use full line width
516 # key too large, use full line width
517 key = key.ljust(width)
517 key = key.ljust(width)
518 else:
518 else:
519 # key fits within field width
519 # key fits within field width
520 key = key.ljust(_fieldwidth)
520 key = key.ljust(_fieldwidth)
521 block['lines'][0] = key + block['lines'][0]
521 block['lines'][0] = key + block['lines'][0]
522 elif block['type'] == 'option':
522 elif block['type'] == 'option':
523 return formatoption(block, width)
523 return formatoption(block, width)
524
524
525 text = ' '.join(map(str.strip, block['lines']))
525 text = ' '.join(map(str.strip, block['lines']))
526 return util.wrap(text, width=width,
526 return util.wrap(text, width=width,
527 initindent=indent,
527 initindent=indent,
528 hangindent=subindent) + '\n'
528 hangindent=subindent) + '\n'
529
529
530 def formathtml(blocks):
530 def formathtml(blocks):
531 """Format RST blocks as HTML"""
531 """Format RST blocks as HTML"""
532
532
533 out = []
533 out = []
534 headernest = ''
534 headernest = ''
535 listnest = []
535 listnest = []
536
536
537 def escape(s):
537 def escape(s):
538 return cgi.escape(s, True)
538 return cgi.escape(s, True)
539
539
540 def openlist(start, level):
540 def openlist(start, level):
541 if not listnest or listnest[-1][0] != start:
541 if not listnest or listnest[-1][0] != start:
542 listnest.append((start, level))
542 listnest.append((start, level))
543 out.append('<%s>\n' % start)
543 out.append('<%s>\n' % start)
544
544
545 blocks = [b for b in blocks if b['type'] != 'margin']
545 blocks = [b for b in blocks if b['type'] != 'margin']
546
546
547 for pos, b in enumerate(blocks):
547 for pos, b in enumerate(blocks):
548 btype = b['type']
548 btype = b['type']
549 level = b['indent']
549 level = b['indent']
550 lines = b['lines']
550 lines = b['lines']
551
551
552 if btype == 'admonition':
552 if btype == 'admonition':
553 admonition = escape(_admonitiontitles[b['admonitiontitle']])
553 admonition = escape(_admonitiontitles[b['admonitiontitle']])
554 text = escape(' '.join(map(str.strip, lines)))
554 text = escape(' '.join(map(str.strip, lines)))
555 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
555 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
556 elif btype == 'paragraph':
556 elif btype == 'paragraph':
557 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
557 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
558 elif btype == 'margin':
558 elif btype == 'margin':
559 pass
559 pass
560 elif btype == 'literal':
560 elif btype == 'literal':
561 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
561 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
562 elif btype == 'section':
562 elif btype == 'section':
563 i = b['underline']
563 i = b['underline']
564 if i not in headernest:
564 if i not in headernest:
565 headernest += i
565 headernest += i
566 level = headernest.index(i) + 1
566 level = headernest.index(i) + 1
567 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
567 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
568 elif btype == 'table':
568 elif btype == 'table':
569 table = b['table']
569 table = b['table']
570 out.append('<table>\n')
570 out.append('<table>\n')
571 for row in table:
571 for row in table:
572 out.append('<tr>')
572 out.append('<tr>')
573 for v in row:
573 for v in row:
574 out.append('<td>')
574 out.append('<td>')
575 out.append(escape(v))
575 out.append(escape(v))
576 out.append('</td>')
576 out.append('</td>')
577 out.append('\n')
577 out.append('\n')
578 out.pop()
578 out.pop()
579 out.append('</tr>\n')
579 out.append('</tr>\n')
580 out.append('</table>\n')
580 out.append('</table>\n')
581 elif btype == 'definition':
581 elif btype == 'definition':
582 openlist('dl', level)
582 openlist('dl', level)
583 term = escape(lines[0])
583 term = escape(lines[0])
584 text = escape(' '.join(map(str.strip, lines[1:])))
584 text = escape(' '.join(map(str.strip, lines[1:])))
585 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
585 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
586 elif btype == 'bullet':
586 elif btype == 'bullet':
587 bullet, head = lines[0].split(' ', 1)
587 bullet, head = lines[0].split(' ', 1)
588 if bullet == '-':
588 if bullet == '-':
589 openlist('ul', level)
589 openlist('ul', level)
590 else:
590 else:
591 openlist('ol', level)
591 openlist('ol', level)
592 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
592 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
593 elif btype == 'field':
593 elif btype == 'field':
594 openlist('dl', level)
594 openlist('dl', level)
595 key = escape(b['key'])
595 key = escape(b['key'])
596 text = escape(' '.join(map(str.strip, lines)))
596 text = escape(' '.join(map(str.strip, lines)))
597 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
597 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
598 elif btype == 'option':
598 elif btype == 'option':
599 openlist('dl', level)
599 openlist('dl', level)
600 opt = escape(b['optstr'])
600 opt = escape(b['optstr'])
601 desc = escape(' '.join(map(str.strip, lines)))
601 desc = escape(' '.join(map(str.strip, lines)))
602 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
602 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
603
603
604 # close lists if indent level of next block is lower
604 # close lists if indent level of next block is lower
605 if listnest:
605 if listnest:
606 start, level = listnest[-1]
606 start, level = listnest[-1]
607 if pos == len(blocks) - 1:
607 if pos == len(blocks) - 1:
608 out.append('</%s>\n' % start)
608 out.append('</%s>\n' % start)
609 listnest.pop()
609 listnest.pop()
610 else:
610 else:
611 nb = blocks[pos + 1]
611 nb = blocks[pos + 1]
612 ni = nb['indent']
612 ni = nb['indent']
613 if (ni < level or
613 if (ni < level or
614 (ni == level and
614 (ni == level and
615 nb['type'] not in 'definition bullet field option')):
615 nb['type'] not in 'definition bullet field option')):
616 out.append('</%s>\n' % start)
616 out.append('</%s>\n' % start)
617 listnest.pop()
617 listnest.pop()
618
618
619 return ''.join(out)
619 return ''.join(out)
620
620
621 def parse(text, indent=0, keep=None):
621 def parse(text, indent=0, keep=None):
622 """Parse text into a list of blocks"""
622 """Parse text into a list of blocks"""
623 pruned = []
623 pruned = []
624 blocks = findblocks(text)
624 blocks = findblocks(text)
625 for b in blocks:
625 for b in blocks:
626 b['indent'] += indent
626 b['indent'] += indent
627 blocks = findliteralblocks(blocks)
627 blocks = findliteralblocks(blocks)
628 blocks = findtables(blocks)
628 blocks = findtables(blocks)
629 blocks, pruned = prunecontainers(blocks, keep or [])
629 blocks, pruned = prunecontainers(blocks, keep or [])
630 blocks = findsections(blocks)
630 blocks = findsections(blocks)
631 blocks = inlineliterals(blocks)
631 blocks = inlineliterals(blocks)
632 blocks = hgrole(blocks)
632 blocks = hgrole(blocks)
633 blocks = splitparagraphs(blocks)
633 blocks = splitparagraphs(blocks)
634 blocks = updatefieldlists(blocks)
634 blocks = updatefieldlists(blocks)
635 blocks = updateoptionlists(blocks)
635 blocks = updateoptionlists(blocks)
636 blocks = findadmonitions(blocks)
636 blocks = addmargins(blocks)
637 blocks = addmargins(blocks)
637 blocks = prunecomments(blocks)
638 blocks = prunecomments(blocks)
638 blocks = findadmonitions(blocks)
639 return blocks, pruned
639 return blocks, pruned
640
640
641 def formatblocks(blocks, width):
641 def formatblocks(blocks, width):
642 text = ''.join(formatblock(b, width) for b in blocks)
642 text = ''.join(formatblock(b, width) for b in blocks)
643 return text
643 return text
644
644
645 def format(text, width=80, indent=0, keep=None, style='plain'):
645 def format(text, width=80, indent=0, keep=None, style='plain'):
646 """Parse and format the text according to width."""
646 """Parse and format the text according to width."""
647 blocks, pruned = parse(text, indent, keep or [])
647 blocks, pruned = parse(text, indent, keep or [])
648 if style == 'html':
648 if style == 'html':
649 text = formathtml(blocks)
649 text = formathtml(blocks)
650 else:
650 else:
651 text = ''.join(formatblock(b, width) for b in blocks)
651 text = ''.join(formatblock(b, width) for b in blocks)
652 if keep is None:
652 if keep is None:
653 return text
653 return text
654 else:
654 else:
655 return text, pruned
655 return text, pruned
656
656
657 def getsections(blocks):
657 def getsections(blocks):
658 '''return a list of (section name, nesting level, blocks) tuples'''
658 '''return a list of (section name, nesting level, blocks) tuples'''
659 nest = ""
659 nest = ""
660 level = 0
660 level = 0
661 secs = []
661 secs = []
662 for b in blocks:
662 for b in blocks:
663 if b['type'] == 'section':
663 if b['type'] == 'section':
664 i = b['underline']
664 i = b['underline']
665 if i not in nest:
665 if i not in nest:
666 nest += i
666 nest += i
667 level = nest.index(i) + 1
667 level = nest.index(i) + 1
668 nest = nest[:level]
668 nest = nest[:level]
669 secs.append((b['lines'][0], level, [b]))
669 secs.append((b['lines'][0], level, [b]))
670 else:
670 else:
671 if not secs:
671 if not secs:
672 # add an initial empty section
672 # add an initial empty section
673 secs = [('', 0, [])]
673 secs = [('', 0, [])]
674 secs[-1][2].append(b)
674 secs[-1][2].append(b)
675 return secs
675 return secs
676
676
677 def decorateblocks(blocks, width):
677 def decorateblocks(blocks, width):
678 '''generate a list of (section name, line text) pairs for search'''
678 '''generate a list of (section name, line text) pairs for search'''
679 lines = []
679 lines = []
680 for s in getsections(blocks):
680 for s in getsections(blocks):
681 section = s[0]
681 section = s[0]
682 text = formatblocks(s[2], width)
682 text = formatblocks(s[2], width)
683 lines.append([(section, l) for l in text.splitlines(True)])
683 lines.append([(section, l) for l in text.splitlines(True)])
684 return lines
684 return lines
685
685
686 def maketable(data, indent=0, header=False):
686 def maketable(data, indent=0, header=False):
687 '''Generate an RST table for the given table data as a list of lines'''
687 '''Generate an RST table for the given table data as a list of lines'''
688
688
689 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
689 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
690 indent = ' ' * indent
690 indent = ' ' * indent
691 div = indent + ' '.join('=' * w for w in widths) + '\n'
691 div = indent + ' '.join('=' * w for w in widths) + '\n'
692
692
693 out = [div]
693 out = [div]
694 for row in data:
694 for row in data:
695 l = []
695 l = []
696 for w, v in zip(widths, row):
696 for w, v in zip(widths, row):
697 pad = ' ' * (w - encoding.colwidth(v))
697 pad = ' ' * (w - encoding.colwidth(v))
698 l.append(v + pad)
698 l.append(v + pad)
699 out.append(indent + ' '.join(l) + "\n")
699 out.append(indent + ' '.join(l) + "\n")
700 if header and len(data) > 1:
700 if header and len(data) > 1:
701 out.insert(2, div)
701 out.insert(2, div)
702 out.append(div)
702 out.append(div)
703 return out
703 return out
General Comments 0
You need to be logged in to leave comments. Login now