##// END OF EJS Templates
minirst: allow multiple container types for prune...
Matt Mackall -
r22584:19bd8bda default
parent child Browse files
Show More
@@ -1,710 +1,713 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.decode("ascii"), t.decode("ascii"))
59 utext = utext.replace(f.decode("ascii"), t.decode("ascii"))
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({'indent': indent, 'lines': lines})
76 blocks.append({'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 own line, not a literal block
112 # directive on its own 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({'type': type, 'lines': [],
177 items.append({'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 = True
265 for c in keep:
266 if c in containertype.split('.'):
267 prune = False
265 if prune:
268 if prune:
266 pruned.append(containertype)
269 pruned.append(containertype)
267
270
268 # Always delete "..container:: type" block
271 # Always delete "..container:: type" block
269 del blocks[i]
272 del blocks[i]
270 j = i
273 j = i
271 i -= 1
274 i -= 1
272 while j < len(blocks) and blocks[j]['indent'] > indent:
275 while j < len(blocks) and blocks[j]['indent'] > indent:
273 if prune:
276 if prune:
274 del blocks[j]
277 del blocks[j]
275 else:
278 else:
276 blocks[j]['indent'] -= adjustment
279 blocks[j]['indent'] -= adjustment
277 j += 1
280 j += 1
278 i += 1
281 i += 1
279 return blocks, pruned
282 return blocks, pruned
280
283
281 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
284 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
282
285
283 def findtables(blocks):
286 def findtables(blocks):
284 '''Find simple tables
287 '''Find simple tables
285
288
286 Only simple one-line table elements are supported
289 Only simple one-line table elements are supported
287 '''
290 '''
288
291
289 for block in blocks:
292 for block in blocks:
290 # Searching for a block that looks like this:
293 # Searching for a block that looks like this:
291 #
294 #
292 # === ==== ===
295 # === ==== ===
293 # A B C
296 # A B C
294 # === ==== === <- optional
297 # === ==== === <- optional
295 # 1 2 3
298 # 1 2 3
296 # x y z
299 # x y z
297 # === ==== ===
300 # === ==== ===
298 if (block['type'] == 'paragraph' and
301 if (block['type'] == 'paragraph' and
299 len(block['lines']) > 2 and
302 len(block['lines']) > 2 and
300 _tablere.match(block['lines'][0]) and
303 _tablere.match(block['lines'][0]) and
301 block['lines'][0] == block['lines'][-1]):
304 block['lines'][0] == block['lines'][-1]):
302 block['type'] = 'table'
305 block['type'] = 'table'
303 block['header'] = False
306 block['header'] = False
304 div = block['lines'][0]
307 div = block['lines'][0]
305
308
306 # column markers are ASCII so we can calculate column
309 # column markers are ASCII so we can calculate column
307 # position in bytes
310 # position in bytes
308 columns = [x for x in xrange(len(div))
311 columns = [x for x in xrange(len(div))
309 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
312 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
310 rows = []
313 rows = []
311 for l in block['lines'][1:-1]:
314 for l in block['lines'][1:-1]:
312 if l == div:
315 if l == div:
313 block['header'] = True
316 block['header'] = True
314 continue
317 continue
315 row = []
318 row = []
316 # we measure columns not in bytes or characters but in
319 # we measure columns not in bytes or characters but in
317 # colwidth which makes things tricky
320 # colwidth which makes things tricky
318 pos = columns[0] # leading whitespace is bytes
321 pos = columns[0] # leading whitespace is bytes
319 for n, start in enumerate(columns):
322 for n, start in enumerate(columns):
320 if n + 1 < len(columns):
323 if n + 1 < len(columns):
321 width = columns[n + 1] - start
324 width = columns[n + 1] - start
322 v = encoding.getcols(l, pos, width) # gather columns
325 v = encoding.getcols(l, pos, width) # gather columns
323 pos += len(v) # calculate byte position of end
326 pos += len(v) # calculate byte position of end
324 row.append(v.strip())
327 row.append(v.strip())
325 else:
328 else:
326 row.append(l[pos:].strip())
329 row.append(l[pos:].strip())
327 rows.append(row)
330 rows.append(row)
328
331
329 block['table'] = rows
332 block['table'] = rows
330
333
331 return blocks
334 return blocks
332
335
333 def findsections(blocks):
336 def findsections(blocks):
334 """Finds sections.
337 """Finds sections.
335
338
336 The blocks must have a 'type' field, i.e., they should have been
339 The blocks must have a 'type' field, i.e., they should have been
337 run through findliteralblocks first.
340 run through findliteralblocks first.
338 """
341 """
339 for block in blocks:
342 for block in blocks:
340 # Searching for a block that looks like this:
343 # Searching for a block that looks like this:
341 #
344 #
342 # +------------------------------+
345 # +------------------------------+
343 # | Section title |
346 # | Section title |
344 # | ------------- |
347 # | ------------- |
345 # +------------------------------+
348 # +------------------------------+
346 if (block['type'] == 'paragraph' and
349 if (block['type'] == 'paragraph' and
347 len(block['lines']) == 2 and
350 len(block['lines']) == 2 and
348 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
351 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
349 _sectionre.match(block['lines'][1])):
352 _sectionre.match(block['lines'][1])):
350 block['underline'] = block['lines'][1][0]
353 block['underline'] = block['lines'][1][0]
351 block['type'] = 'section'
354 block['type'] = 'section'
352 del block['lines'][1]
355 del block['lines'][1]
353 return blocks
356 return blocks
354
357
355 def inlineliterals(blocks):
358 def inlineliterals(blocks):
356 substs = [('``', '"')]
359 substs = [('``', '"')]
357 for b in blocks:
360 for b in blocks:
358 if b['type'] in ('paragraph', 'section'):
361 if b['type'] in ('paragraph', 'section'):
359 b['lines'] = [replace(l, substs) for l in b['lines']]
362 b['lines'] = [replace(l, substs) for l in b['lines']]
360 return blocks
363 return blocks
361
364
362 def hgrole(blocks):
365 def hgrole(blocks):
363 substs = [(':hg:`', '"hg '), ('`', '"')]
366 substs = [(':hg:`', '"hg '), ('`', '"')]
364 for b in blocks:
367 for b in blocks:
365 if b['type'] in ('paragraph', 'section'):
368 if b['type'] in ('paragraph', 'section'):
366 # Turn :hg:`command` into "hg command". This also works
369 # Turn :hg:`command` into "hg command". This also works
367 # when there is a line break in the command and relies on
370 # 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
371 # the fact that we have no stray back-quotes in the input
369 # (run the blocks through inlineliterals first).
372 # (run the blocks through inlineliterals first).
370 b['lines'] = [replace(l, substs) for l in b['lines']]
373 b['lines'] = [replace(l, substs) for l in b['lines']]
371 return blocks
374 return blocks
372
375
373 def addmargins(blocks):
376 def addmargins(blocks):
374 """Adds empty blocks for vertical spacing.
377 """Adds empty blocks for vertical spacing.
375
378
376 This groups bullets, options, and definitions together with no vertical
379 This groups bullets, options, and definitions together with no vertical
377 space between them, and adds an empty block between all other blocks.
380 space between them, and adds an empty block between all other blocks.
378 """
381 """
379 i = 1
382 i = 1
380 while i < len(blocks):
383 while i < len(blocks):
381 if (blocks[i]['type'] == blocks[i - 1]['type'] and
384 if (blocks[i]['type'] == blocks[i - 1]['type'] and
382 blocks[i]['type'] in ('bullet', 'option', 'field')):
385 blocks[i]['type'] in ('bullet', 'option', 'field')):
383 i += 1
386 i += 1
384 elif not blocks[i - 1]['lines']:
387 elif not blocks[i - 1]['lines']:
385 # no lines in previous block, do not separate
388 # no lines in previous block, do not separate
386 i += 1
389 i += 1
387 else:
390 else:
388 blocks.insert(i, {'lines': [''], 'indent': 0, 'type': 'margin'})
391 blocks.insert(i, {'lines': [''], 'indent': 0, 'type': 'margin'})
389 i += 2
392 i += 2
390 return blocks
393 return blocks
391
394
392 def prunecomments(blocks):
395 def prunecomments(blocks):
393 """Remove comments."""
396 """Remove comments."""
394 i = 0
397 i = 0
395 while i < len(blocks):
398 while i < len(blocks):
396 b = blocks[i]
399 b = blocks[i]
397 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
400 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
398 b['lines'] == ['..']):
401 b['lines'] == ['..']):
399 del blocks[i]
402 del blocks[i]
400 if i < len(blocks) and blocks[i]['type'] == 'margin':
403 if i < len(blocks) and blocks[i]['type'] == 'margin':
401 del blocks[i]
404 del blocks[i]
402 else:
405 else:
403 i += 1
406 i += 1
404 return blocks
407 return blocks
405
408
406 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
409 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
407 r"error|hint|important|note|tip|warning)::",
410 r"error|hint|important|note|tip|warning)::",
408 flags=re.IGNORECASE)
411 flags=re.IGNORECASE)
409
412
410 def findadmonitions(blocks):
413 def findadmonitions(blocks):
411 """
414 """
412 Makes the type of the block an admonition block if
415 Makes the type of the block an admonition block if
413 the first line is an admonition directive
416 the first line is an admonition directive
414 """
417 """
415 i = 0
418 i = 0
416 while i < len(blocks):
419 while i < len(blocks):
417 m = _admonitionre.match(blocks[i]['lines'][0])
420 m = _admonitionre.match(blocks[i]['lines'][0])
418 if m:
421 if m:
419 blocks[i]['type'] = 'admonition'
422 blocks[i]['type'] = 'admonition'
420 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
423 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
421
424
422 firstline = blocks[i]['lines'][0][m.end() + 1:]
425 firstline = blocks[i]['lines'][0][m.end() + 1:]
423 if firstline:
426 if firstline:
424 blocks[i]['lines'].insert(1, ' ' + firstline)
427 blocks[i]['lines'].insert(1, ' ' + firstline)
425
428
426 blocks[i]['admonitiontitle'] = admonitiontitle
429 blocks[i]['admonitiontitle'] = admonitiontitle
427 del blocks[i]['lines'][0]
430 del blocks[i]['lines'][0]
428 i = i + 1
431 i = i + 1
429 return blocks
432 return blocks
430
433
431 _admonitiontitles = {'attention': _('Attention:'),
434 _admonitiontitles = {'attention': _('Attention:'),
432 'caution': _('Caution:'),
435 'caution': _('Caution:'),
433 'danger': _('!Danger!') ,
436 'danger': _('!Danger!') ,
434 'error': _('Error:'),
437 'error': _('Error:'),
435 'hint': _('Hint:'),
438 'hint': _('Hint:'),
436 'important': _('Important:'),
439 'important': _('Important:'),
437 'note': _('Note:'),
440 'note': _('Note:'),
438 'tip': _('Tip:'),
441 'tip': _('Tip:'),
439 'warning': _('Warning!')}
442 'warning': _('Warning!')}
440
443
441 def formatoption(block, width):
444 def formatoption(block, width):
442 desc = ' '.join(map(str.strip, block['lines']))
445 desc = ' '.join(map(str.strip, block['lines']))
443 colwidth = encoding.colwidth(block['optstr'])
446 colwidth = encoding.colwidth(block['optstr'])
444 usablewidth = width - 1
447 usablewidth = width - 1
445 hanging = block['optstrwidth']
448 hanging = block['optstrwidth']
446 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
449 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
447 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
450 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
448 return ' %s\n' % (util.wrap(desc, usablewidth,
451 return ' %s\n' % (util.wrap(desc, usablewidth,
449 initindent=initindent,
452 initindent=initindent,
450 hangindent=hangindent))
453 hangindent=hangindent))
451
454
452 def formatblock(block, width):
455 def formatblock(block, width):
453 """Format a block according to width."""
456 """Format a block according to width."""
454 if width <= 0:
457 if width <= 0:
455 width = 78
458 width = 78
456 indent = ' ' * block['indent']
459 indent = ' ' * block['indent']
457 if block['type'] == 'admonition':
460 if block['type'] == 'admonition':
458 admonition = _admonitiontitles[block['admonitiontitle']]
461 admonition = _admonitiontitles[block['admonitiontitle']]
459 if not block['lines']:
462 if not block['lines']:
460 return indent + admonition + '\n'
463 return indent + admonition + '\n'
461 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
464 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
462
465
463 defindent = indent + hang * ' '
466 defindent = indent + hang * ' '
464 text = ' '.join(map(str.strip, block['lines']))
467 text = ' '.join(map(str.strip, block['lines']))
465 return '%s\n%s\n' % (indent + admonition,
468 return '%s\n%s\n' % (indent + admonition,
466 util.wrap(text, width=width,
469 util.wrap(text, width=width,
467 initindent=defindent,
470 initindent=defindent,
468 hangindent=defindent))
471 hangindent=defindent))
469 if block['type'] == 'margin':
472 if block['type'] == 'margin':
470 return '\n'
473 return '\n'
471 if block['type'] == 'literal':
474 if block['type'] == 'literal':
472 indent += ' '
475 indent += ' '
473 return indent + ('\n' + indent).join(block['lines']) + '\n'
476 return indent + ('\n' + indent).join(block['lines']) + '\n'
474 if block['type'] == 'section':
477 if block['type'] == 'section':
475 underline = encoding.colwidth(block['lines'][0]) * block['underline']
478 underline = encoding.colwidth(block['lines'][0]) * block['underline']
476 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
479 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
477 if block['type'] == 'table':
480 if block['type'] == 'table':
478 table = block['table']
481 table = block['table']
479 # compute column widths
482 # compute column widths
480 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
483 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
481 text = ''
484 text = ''
482 span = sum(widths) + len(widths) - 1
485 span = sum(widths) + len(widths) - 1
483 indent = ' ' * block['indent']
486 indent = ' ' * block['indent']
484 hang = ' ' * (len(indent) + span - widths[-1])
487 hang = ' ' * (len(indent) + span - widths[-1])
485
488
486 for row in table:
489 for row in table:
487 l = []
490 l = []
488 for w, v in zip(widths, row):
491 for w, v in zip(widths, row):
489 pad = ' ' * (w - encoding.colwidth(v))
492 pad = ' ' * (w - encoding.colwidth(v))
490 l.append(v + pad)
493 l.append(v + pad)
491 l = ' '.join(l)
494 l = ' '.join(l)
492 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
495 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
493 if not text and block['header']:
496 if not text and block['header']:
494 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
497 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
495 else:
498 else:
496 text += l + "\n"
499 text += l + "\n"
497 return text
500 return text
498 if block['type'] == 'definition':
501 if block['type'] == 'definition':
499 term = indent + block['lines'][0]
502 term = indent + block['lines'][0]
500 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
503 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
501 defindent = indent + hang * ' '
504 defindent = indent + hang * ' '
502 text = ' '.join(map(str.strip, block['lines'][1:]))
505 text = ' '.join(map(str.strip, block['lines'][1:]))
503 return '%s\n%s\n' % (term, util.wrap(text, width=width,
506 return '%s\n%s\n' % (term, util.wrap(text, width=width,
504 initindent=defindent,
507 initindent=defindent,
505 hangindent=defindent))
508 hangindent=defindent))
506 subindent = indent
509 subindent = indent
507 if block['type'] == 'bullet':
510 if block['type'] == 'bullet':
508 if block['lines'][0].startswith('| '):
511 if block['lines'][0].startswith('| '):
509 # Remove bullet for line blocks and add no extra
512 # Remove bullet for line blocks and add no extra
510 # indention.
513 # indention.
511 block['lines'][0] = block['lines'][0][2:]
514 block['lines'][0] = block['lines'][0][2:]
512 else:
515 else:
513 m = _bulletre.match(block['lines'][0])
516 m = _bulletre.match(block['lines'][0])
514 subindent = indent + m.end() * ' '
517 subindent = indent + m.end() * ' '
515 elif block['type'] == 'field':
518 elif block['type'] == 'field':
516 key = block['key']
519 key = block['key']
517 subindent = indent + _fieldwidth * ' '
520 subindent = indent + _fieldwidth * ' '
518 if len(key) + 2 > _fieldwidth:
521 if len(key) + 2 > _fieldwidth:
519 # key too large, use full line width
522 # key too large, use full line width
520 key = key.ljust(width)
523 key = key.ljust(width)
521 else:
524 else:
522 # key fits within field width
525 # key fits within field width
523 key = key.ljust(_fieldwidth)
526 key = key.ljust(_fieldwidth)
524 block['lines'][0] = key + block['lines'][0]
527 block['lines'][0] = key + block['lines'][0]
525 elif block['type'] == 'option':
528 elif block['type'] == 'option':
526 return formatoption(block, width)
529 return formatoption(block, width)
527
530
528 text = ' '.join(map(str.strip, block['lines']))
531 text = ' '.join(map(str.strip, block['lines']))
529 return util.wrap(text, width=width,
532 return util.wrap(text, width=width,
530 initindent=indent,
533 initindent=indent,
531 hangindent=subindent) + '\n'
534 hangindent=subindent) + '\n'
532
535
533 def formathtml(blocks):
536 def formathtml(blocks):
534 """Format RST blocks as HTML"""
537 """Format RST blocks as HTML"""
535
538
536 out = []
539 out = []
537 headernest = ''
540 headernest = ''
538 listnest = []
541 listnest = []
539
542
540 def escape(s):
543 def escape(s):
541 return cgi.escape(s, True)
544 return cgi.escape(s, True)
542
545
543 def openlist(start, level):
546 def openlist(start, level):
544 if not listnest or listnest[-1][0] != start:
547 if not listnest or listnest[-1][0] != start:
545 listnest.append((start, level))
548 listnest.append((start, level))
546 out.append('<%s>\n' % start)
549 out.append('<%s>\n' % start)
547
550
548 blocks = [b for b in blocks if b['type'] != 'margin']
551 blocks = [b for b in blocks if b['type'] != 'margin']
549
552
550 for pos, b in enumerate(blocks):
553 for pos, b in enumerate(blocks):
551 btype = b['type']
554 btype = b['type']
552 level = b['indent']
555 level = b['indent']
553 lines = b['lines']
556 lines = b['lines']
554
557
555 if btype == 'admonition':
558 if btype == 'admonition':
556 admonition = escape(_admonitiontitles[b['admonitiontitle']])
559 admonition = escape(_admonitiontitles[b['admonitiontitle']])
557 text = escape(' '.join(map(str.strip, lines)))
560 text = escape(' '.join(map(str.strip, lines)))
558 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
561 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
559 elif btype == 'paragraph':
562 elif btype == 'paragraph':
560 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
563 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
561 elif btype == 'margin':
564 elif btype == 'margin':
562 pass
565 pass
563 elif btype == 'literal':
566 elif btype == 'literal':
564 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
567 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
565 elif btype == 'section':
568 elif btype == 'section':
566 i = b['underline']
569 i = b['underline']
567 if i not in headernest:
570 if i not in headernest:
568 headernest += i
571 headernest += i
569 level = headernest.index(i) + 1
572 level = headernest.index(i) + 1
570 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
573 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
571 elif btype == 'table':
574 elif btype == 'table':
572 table = b['table']
575 table = b['table']
573 out.append('<table>\n')
576 out.append('<table>\n')
574 for row in table:
577 for row in table:
575 out.append('<tr>')
578 out.append('<tr>')
576 for v in row:
579 for v in row:
577 out.append('<td>')
580 out.append('<td>')
578 out.append(escape(v))
581 out.append(escape(v))
579 out.append('</td>')
582 out.append('</td>')
580 out.append('\n')
583 out.append('\n')
581 out.pop()
584 out.pop()
582 out.append('</tr>\n')
585 out.append('</tr>\n')
583 out.append('</table>\n')
586 out.append('</table>\n')
584 elif btype == 'definition':
587 elif btype == 'definition':
585 openlist('dl', level)
588 openlist('dl', level)
586 term = escape(lines[0])
589 term = escape(lines[0])
587 text = escape(' '.join(map(str.strip, lines[1:])))
590 text = escape(' '.join(map(str.strip, lines[1:])))
588 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
591 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
589 elif btype == 'bullet':
592 elif btype == 'bullet':
590 bullet, head = lines[0].split(' ', 1)
593 bullet, head = lines[0].split(' ', 1)
591 if bullet == '-':
594 if bullet == '-':
592 openlist('ul', level)
595 openlist('ul', level)
593 else:
596 else:
594 openlist('ol', level)
597 openlist('ol', level)
595 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
598 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
596 elif btype == 'field':
599 elif btype == 'field':
597 openlist('dl', level)
600 openlist('dl', level)
598 key = escape(b['key'])
601 key = escape(b['key'])
599 text = escape(' '.join(map(str.strip, lines)))
602 text = escape(' '.join(map(str.strip, lines)))
600 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
603 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
601 elif btype == 'option':
604 elif btype == 'option':
602 openlist('dl', level)
605 openlist('dl', level)
603 opt = escape(b['optstr'])
606 opt = escape(b['optstr'])
604 desc = escape(' '.join(map(str.strip, lines)))
607 desc = escape(' '.join(map(str.strip, lines)))
605 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
608 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
606
609
607 # close lists if indent level of next block is lower
610 # close lists if indent level of next block is lower
608 if listnest:
611 if listnest:
609 start, level = listnest[-1]
612 start, level = listnest[-1]
610 if pos == len(blocks) - 1:
613 if pos == len(blocks) - 1:
611 out.append('</%s>\n' % start)
614 out.append('</%s>\n' % start)
612 listnest.pop()
615 listnest.pop()
613 else:
616 else:
614 nb = blocks[pos + 1]
617 nb = blocks[pos + 1]
615 ni = nb['indent']
618 ni = nb['indent']
616 if (ni < level or
619 if (ni < level or
617 (ni == level and
620 (ni == level and
618 nb['type'] not in 'definition bullet field option')):
621 nb['type'] not in 'definition bullet field option')):
619 out.append('</%s>\n' % start)
622 out.append('</%s>\n' % start)
620 listnest.pop()
623 listnest.pop()
621
624
622 return ''.join(out)
625 return ''.join(out)
623
626
624 def parse(text, indent=0, keep=None):
627 def parse(text, indent=0, keep=None):
625 """Parse text into a list of blocks"""
628 """Parse text into a list of blocks"""
626 pruned = []
629 pruned = []
627 blocks = findblocks(text)
630 blocks = findblocks(text)
628 for b in blocks:
631 for b in blocks:
629 b['indent'] += indent
632 b['indent'] += indent
630 blocks = findliteralblocks(blocks)
633 blocks = findliteralblocks(blocks)
631 blocks = findtables(blocks)
634 blocks = findtables(blocks)
632 blocks, pruned = prunecontainers(blocks, keep or [])
635 blocks, pruned = prunecontainers(blocks, keep or [])
633 blocks = findsections(blocks)
636 blocks = findsections(blocks)
634 blocks = inlineliterals(blocks)
637 blocks = inlineliterals(blocks)
635 blocks = hgrole(blocks)
638 blocks = hgrole(blocks)
636 blocks = splitparagraphs(blocks)
639 blocks = splitparagraphs(blocks)
637 blocks = updatefieldlists(blocks)
640 blocks = updatefieldlists(blocks)
638 blocks = updateoptionlists(blocks)
641 blocks = updateoptionlists(blocks)
639 blocks = findadmonitions(blocks)
642 blocks = findadmonitions(blocks)
640 blocks = addmargins(blocks)
643 blocks = addmargins(blocks)
641 blocks = prunecomments(blocks)
644 blocks = prunecomments(blocks)
642 return blocks, pruned
645 return blocks, pruned
643
646
644 def formatblocks(blocks, width):
647 def formatblocks(blocks, width):
645 text = ''.join(formatblock(b, width) for b in blocks)
648 text = ''.join(formatblock(b, width) for b in blocks)
646 return text
649 return text
647
650
648 def format(text, width=80, indent=0, keep=None, style='plain'):
651 def format(text, width=80, indent=0, keep=None, style='plain'):
649 """Parse and format the text according to width."""
652 """Parse and format the text according to width."""
650 blocks, pruned = parse(text, indent, keep or [])
653 blocks, pruned = parse(text, indent, keep or [])
651 if style == 'html':
654 if style == 'html':
652 text = formathtml(blocks)
655 text = formathtml(blocks)
653 else:
656 else:
654 text = ''.join(formatblock(b, width) for b in blocks)
657 text = ''.join(formatblock(b, width) for b in blocks)
655 if keep is None:
658 if keep is None:
656 return text
659 return text
657 else:
660 else:
658 return text, pruned
661 return text, pruned
659
662
660 def getsections(blocks):
663 def getsections(blocks):
661 '''return a list of (section name, nesting level, blocks) tuples'''
664 '''return a list of (section name, nesting level, blocks) tuples'''
662 nest = ""
665 nest = ""
663 level = 0
666 level = 0
664 secs = []
667 secs = []
665 for b in blocks:
668 for b in blocks:
666 if b['type'] == 'section':
669 if b['type'] == 'section':
667 i = b['underline']
670 i = b['underline']
668 if i not in nest:
671 if i not in nest:
669 nest += i
672 nest += i
670 level = nest.index(i) + 1
673 level = nest.index(i) + 1
671 nest = nest[:level]
674 nest = nest[:level]
672 secs.append((b['lines'][0], level, [b]))
675 secs.append((b['lines'][0], level, [b]))
673 else:
676 else:
674 if not secs:
677 if not secs:
675 # add an initial empty section
678 # add an initial empty section
676 secs = [('', 0, [])]
679 secs = [('', 0, [])]
677 secs[-1][2].append(b)
680 secs[-1][2].append(b)
678 return secs
681 return secs
679
682
680 def decorateblocks(blocks, width):
683 def decorateblocks(blocks, width):
681 '''generate a list of (section name, line text) pairs for search'''
684 '''generate a list of (section name, line text) pairs for search'''
682 lines = []
685 lines = []
683 for s in getsections(blocks):
686 for s in getsections(blocks):
684 section = s[0]
687 section = s[0]
685 text = formatblocks(s[2], width)
688 text = formatblocks(s[2], width)
686 lines.append([(section, l) for l in text.splitlines(True)])
689 lines.append([(section, l) for l in text.splitlines(True)])
687 return lines
690 return lines
688
691
689 def maketable(data, indent=0, header=False):
692 def maketable(data, indent=0, header=False):
690 '''Generate an RST table for the given table data as a list of lines'''
693 '''Generate an RST table for the given table data as a list of lines'''
691
694
692 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
695 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
693 indent = ' ' * indent
696 indent = ' ' * indent
694 div = indent + ' '.join('=' * w for w in widths) + '\n'
697 div = indent + ' '.join('=' * w for w in widths) + '\n'
695
698
696 out = [div]
699 out = [div]
697 for row in data:
700 for row in data:
698 l = []
701 l = []
699 for w, v in zip(widths, row):
702 for w, v in zip(widths, row):
700 if '\n' in v:
703 if '\n' in v:
701 # only remove line breaks and indentation, long lines are
704 # only remove line breaks and indentation, long lines are
702 # handled by the next tool
705 # handled by the next tool
703 v = ' '.join(e.lstrip() for e in v.split('\n'))
706 v = ' '.join(e.lstrip() for e in v.split('\n'))
704 pad = ' ' * (w - encoding.colwidth(v))
707 pad = ' ' * (w - encoding.colwidth(v))
705 l.append(v + pad)
708 l.append(v + pad)
706 out.append(indent + ' '.join(l) + "\n")
709 out.append(indent + ' '.join(l) + "\n")
707 if header and len(data) > 1:
710 if header and len(data) > 1:
708 out.insert(2, div)
711 out.insert(2, div)
709 out.append(div)
712 out.append(div)
710 return out
713 return out
General Comments 0
You need to be logged in to leave comments. Login now