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