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