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