##// END OF EJS Templates
minirst: grab a byte, not an int, for the underline style
Augie Fackler -
r32525:043c147c default
parent child Browse files
Show More
@@ -1,821 +1,821 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 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:1]
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 def findadmonitions(blocks, admonitions=None):
416 def findadmonitions(blocks, admonitions=None):
417 """
417 """
418 Makes the type of the block an admonition block if
418 Makes the type of the block an admonition block if
419 the first line is an admonition directive
419 the first line is an admonition directive
420 """
420 """
421 admonitions = admonitions or _admonitiontitles.keys()
421 admonitions = admonitions or _admonitiontitles.keys()
422
422
423 admonitionre = re.compile(br'\.\. (%s)::' % '|'.join(sorted(admonitions)),
423 admonitionre = re.compile(br'\.\. (%s)::' % '|'.join(sorted(admonitions)),
424 flags=re.IGNORECASE)
424 flags=re.IGNORECASE)
425
425
426 i = 0
426 i = 0
427 while i < len(blocks):
427 while i < len(blocks):
428 m = admonitionre.match(blocks[i]['lines'][0])
428 m = admonitionre.match(blocks[i]['lines'][0])
429 if m:
429 if m:
430 blocks[i]['type'] = 'admonition'
430 blocks[i]['type'] = 'admonition'
431 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
431 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
432
432
433 firstline = blocks[i]['lines'][0][m.end() + 1:]
433 firstline = blocks[i]['lines'][0][m.end() + 1:]
434 if firstline:
434 if firstline:
435 blocks[i]['lines'].insert(1, ' ' + firstline)
435 blocks[i]['lines'].insert(1, ' ' + firstline)
436
436
437 blocks[i]['admonitiontitle'] = admonitiontitle
437 blocks[i]['admonitiontitle'] = admonitiontitle
438 del blocks[i]['lines'][0]
438 del blocks[i]['lines'][0]
439 i = i + 1
439 i = i + 1
440 return blocks
440 return blocks
441
441
442 _admonitiontitles = {
442 _admonitiontitles = {
443 'attention': _('Attention:'),
443 'attention': _('Attention:'),
444 'caution': _('Caution:'),
444 'caution': _('Caution:'),
445 'danger': _('!Danger!'),
445 'danger': _('!Danger!'),
446 'error': _('Error:'),
446 'error': _('Error:'),
447 'hint': _('Hint:'),
447 'hint': _('Hint:'),
448 'important': _('Important:'),
448 'important': _('Important:'),
449 'note': _('Note:'),
449 'note': _('Note:'),
450 'tip': _('Tip:'),
450 'tip': _('Tip:'),
451 'warning': _('Warning!'),
451 'warning': _('Warning!'),
452 }
452 }
453
453
454 def formatoption(block, width):
454 def formatoption(block, width):
455 desc = ' '.join(map(bytes.strip, block['lines']))
455 desc = ' '.join(map(bytes.strip, block['lines']))
456 colwidth = encoding.colwidth(block['optstr'])
456 colwidth = encoding.colwidth(block['optstr'])
457 usablewidth = width - 1
457 usablewidth = width - 1
458 hanging = block['optstrwidth']
458 hanging = block['optstrwidth']
459 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
459 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
460 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
460 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
461 return ' %s\n' % (util.wrap(desc, usablewidth,
461 return ' %s\n' % (util.wrap(desc, usablewidth,
462 initindent=initindent,
462 initindent=initindent,
463 hangindent=hangindent))
463 hangindent=hangindent))
464
464
465 def formatblock(block, width):
465 def formatblock(block, width):
466 """Format a block according to width."""
466 """Format a block according to width."""
467 if width <= 0:
467 if width <= 0:
468 width = 78
468 width = 78
469 indent = ' ' * block['indent']
469 indent = ' ' * block['indent']
470 if block['type'] == 'admonition':
470 if block['type'] == 'admonition':
471 admonition = _admonitiontitles[block['admonitiontitle']]
471 admonition = _admonitiontitles[block['admonitiontitle']]
472 if not block['lines']:
472 if not block['lines']:
473 return indent + admonition + '\n'
473 return indent + admonition + '\n'
474 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
474 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
475
475
476 defindent = indent + hang * ' '
476 defindent = indent + hang * ' '
477 text = ' '.join(map(bytes.strip, block['lines']))
477 text = ' '.join(map(bytes.strip, block['lines']))
478 return '%s\n%s\n' % (indent + admonition,
478 return '%s\n%s\n' % (indent + admonition,
479 util.wrap(text, width=width,
479 util.wrap(text, width=width,
480 initindent=defindent,
480 initindent=defindent,
481 hangindent=defindent))
481 hangindent=defindent))
482 if block['type'] == 'margin':
482 if block['type'] == 'margin':
483 return '\n'
483 return '\n'
484 if block['type'] == 'literal':
484 if block['type'] == 'literal':
485 indent += ' '
485 indent += ' '
486 return indent + ('\n' + indent).join(block['lines']) + '\n'
486 return indent + ('\n' + indent).join(block['lines']) + '\n'
487 if block['type'] == 'section':
487 if block['type'] == 'section':
488 underline = encoding.colwidth(block['lines'][0]) * block['underline']
488 underline = encoding.colwidth(block['lines'][0]) * block['underline']
489 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
489 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
490 if block['type'] == 'table':
490 if block['type'] == 'table':
491 table = block['table']
491 table = block['table']
492 # compute column widths
492 # compute column widths
493 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
493 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
494 text = ''
494 text = ''
495 span = sum(widths) + len(widths) - 1
495 span = sum(widths) + len(widths) - 1
496 indent = ' ' * block['indent']
496 indent = ' ' * block['indent']
497 hang = ' ' * (len(indent) + span - widths[-1])
497 hang = ' ' * (len(indent) + span - widths[-1])
498
498
499 for row in table:
499 for row in table:
500 l = []
500 l = []
501 for w, v in zip(widths, row):
501 for w, v in zip(widths, row):
502 pad = ' ' * (w - encoding.colwidth(v))
502 pad = ' ' * (w - encoding.colwidth(v))
503 l.append(v + pad)
503 l.append(v + pad)
504 l = ' '.join(l)
504 l = ' '.join(l)
505 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
505 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
506 if not text and block['header']:
506 if not text and block['header']:
507 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
507 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
508 else:
508 else:
509 text += l + "\n"
509 text += l + "\n"
510 return text
510 return text
511 if block['type'] == 'definition':
511 if block['type'] == 'definition':
512 term = indent + block['lines'][0]
512 term = indent + block['lines'][0]
513 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
513 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
514 defindent = indent + hang * ' '
514 defindent = indent + hang * ' '
515 text = ' '.join(map(bytes.strip, block['lines'][1:]))
515 text = ' '.join(map(bytes.strip, block['lines'][1:]))
516 return '%s\n%s\n' % (term, util.wrap(text, width=width,
516 return '%s\n%s\n' % (term, util.wrap(text, width=width,
517 initindent=defindent,
517 initindent=defindent,
518 hangindent=defindent))
518 hangindent=defindent))
519 subindent = indent
519 subindent = indent
520 if block['type'] == 'bullet':
520 if block['type'] == 'bullet':
521 if block['lines'][0].startswith('| '):
521 if block['lines'][0].startswith('| '):
522 # Remove bullet for line blocks and add no extra
522 # Remove bullet for line blocks and add no extra
523 # indentation.
523 # indentation.
524 block['lines'][0] = block['lines'][0][2:]
524 block['lines'][0] = block['lines'][0][2:]
525 else:
525 else:
526 m = _bulletre.match(block['lines'][0])
526 m = _bulletre.match(block['lines'][0])
527 subindent = indent + m.end() * ' '
527 subindent = indent + m.end() * ' '
528 elif block['type'] == 'field':
528 elif block['type'] == 'field':
529 key = block['key']
529 key = block['key']
530 subindent = indent + _fieldwidth * ' '
530 subindent = indent + _fieldwidth * ' '
531 if len(key) + 2 > _fieldwidth:
531 if len(key) + 2 > _fieldwidth:
532 # key too large, use full line width
532 # key too large, use full line width
533 key = key.ljust(width)
533 key = key.ljust(width)
534 else:
534 else:
535 # key fits within field width
535 # key fits within field width
536 key = key.ljust(_fieldwidth)
536 key = key.ljust(_fieldwidth)
537 block['lines'][0] = key + block['lines'][0]
537 block['lines'][0] = key + block['lines'][0]
538 elif block['type'] == 'option':
538 elif block['type'] == 'option':
539 return formatoption(block, width)
539 return formatoption(block, width)
540
540
541 text = ' '.join(map(bytes.strip, block['lines']))
541 text = ' '.join(map(bytes.strip, block['lines']))
542 return util.wrap(text, width=width,
542 return util.wrap(text, width=width,
543 initindent=indent,
543 initindent=indent,
544 hangindent=subindent) + '\n'
544 hangindent=subindent) + '\n'
545
545
546 def formathtml(blocks):
546 def formathtml(blocks):
547 """Format RST blocks as HTML"""
547 """Format RST blocks as HTML"""
548
548
549 out = []
549 out = []
550 headernest = ''
550 headernest = ''
551 listnest = []
551 listnest = []
552
552
553 def escape(s):
553 def escape(s):
554 return cgi.escape(s, True)
554 return cgi.escape(s, True)
555
555
556 def openlist(start, level):
556 def openlist(start, level):
557 if not listnest or listnest[-1][0] != start:
557 if not listnest or listnest[-1][0] != start:
558 listnest.append((start, level))
558 listnest.append((start, level))
559 out.append('<%s>\n' % start)
559 out.append('<%s>\n' % start)
560
560
561 blocks = [b for b in blocks if b['type'] != 'margin']
561 blocks = [b for b in blocks if b['type'] != 'margin']
562
562
563 for pos, b in enumerate(blocks):
563 for pos, b in enumerate(blocks):
564 btype = b['type']
564 btype = b['type']
565 level = b['indent']
565 level = b['indent']
566 lines = b['lines']
566 lines = b['lines']
567
567
568 if btype == 'admonition':
568 if btype == 'admonition':
569 admonition = escape(_admonitiontitles[b['admonitiontitle']])
569 admonition = escape(_admonitiontitles[b['admonitiontitle']])
570 text = escape(' '.join(map(bytes.strip, lines)))
570 text = escape(' '.join(map(bytes.strip, lines)))
571 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
571 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
572 elif btype == 'paragraph':
572 elif btype == 'paragraph':
573 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
573 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
574 elif btype == 'margin':
574 elif btype == 'margin':
575 pass
575 pass
576 elif btype == 'literal':
576 elif btype == 'literal':
577 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
577 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
578 elif btype == 'section':
578 elif btype == 'section':
579 i = b['underline']
579 i = b['underline']
580 if i not in headernest:
580 if i not in headernest:
581 headernest += i
581 headernest += i
582 level = headernest.index(i) + 1
582 level = headernest.index(i) + 1
583 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
583 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
584 elif btype == 'table':
584 elif btype == 'table':
585 table = b['table']
585 table = b['table']
586 out.append('<table>\n')
586 out.append('<table>\n')
587 for row in table:
587 for row in table:
588 out.append('<tr>')
588 out.append('<tr>')
589 for v in row:
589 for v in row:
590 out.append('<td>')
590 out.append('<td>')
591 out.append(escape(v))
591 out.append(escape(v))
592 out.append('</td>')
592 out.append('</td>')
593 out.append('\n')
593 out.append('\n')
594 out.pop()
594 out.pop()
595 out.append('</tr>\n')
595 out.append('</tr>\n')
596 out.append('</table>\n')
596 out.append('</table>\n')
597 elif btype == 'definition':
597 elif btype == 'definition':
598 openlist('dl', level)
598 openlist('dl', level)
599 term = escape(lines[0])
599 term = escape(lines[0])
600 text = escape(' '.join(map(bytes.strip, lines[1:])))
600 text = escape(' '.join(map(bytes.strip, lines[1:])))
601 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
601 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
602 elif btype == 'bullet':
602 elif btype == 'bullet':
603 bullet, head = lines[0].split(' ', 1)
603 bullet, head = lines[0].split(' ', 1)
604 if bullet in ('*', '-'):
604 if bullet in ('*', '-'):
605 openlist('ul', level)
605 openlist('ul', level)
606 else:
606 else:
607 openlist('ol', level)
607 openlist('ol', level)
608 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
608 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
609 elif btype == 'field':
609 elif btype == 'field':
610 openlist('dl', level)
610 openlist('dl', level)
611 key = escape(b['key'])
611 key = escape(b['key'])
612 text = escape(' '.join(map(bytes.strip, lines)))
612 text = escape(' '.join(map(bytes.strip, lines)))
613 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
613 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
614 elif btype == 'option':
614 elif btype == 'option':
615 openlist('dl', level)
615 openlist('dl', level)
616 opt = escape(b['optstr'])
616 opt = escape(b['optstr'])
617 desc = escape(' '.join(map(bytes.strip, lines)))
617 desc = escape(' '.join(map(bytes.strip, lines)))
618 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
618 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
619
619
620 # close lists if indent level of next block is lower
620 # close lists if indent level of next block is lower
621 if listnest:
621 if listnest:
622 start, level = listnest[-1]
622 start, level = listnest[-1]
623 if pos == len(blocks) - 1:
623 if pos == len(blocks) - 1:
624 out.append('</%s>\n' % start)
624 out.append('</%s>\n' % start)
625 listnest.pop()
625 listnest.pop()
626 else:
626 else:
627 nb = blocks[pos + 1]
627 nb = blocks[pos + 1]
628 ni = nb['indent']
628 ni = nb['indent']
629 if (ni < level or
629 if (ni < level or
630 (ni == level and
630 (ni == level and
631 nb['type'] not in 'definition bullet field option')):
631 nb['type'] not in 'definition bullet field option')):
632 out.append('</%s>\n' % start)
632 out.append('</%s>\n' % start)
633 listnest.pop()
633 listnest.pop()
634
634
635 return ''.join(out)
635 return ''.join(out)
636
636
637 def parse(text, indent=0, keep=None, admonitions=None):
637 def parse(text, indent=0, keep=None, admonitions=None):
638 """Parse text into a list of blocks"""
638 """Parse text into a list of blocks"""
639 pruned = []
639 pruned = []
640 blocks = findblocks(text)
640 blocks = findblocks(text)
641 for b in blocks:
641 for b in blocks:
642 b['indent'] += indent
642 b['indent'] += indent
643 blocks = findliteralblocks(blocks)
643 blocks = findliteralblocks(blocks)
644 blocks = findtables(blocks)
644 blocks = findtables(blocks)
645 blocks, pruned = prunecontainers(blocks, keep or [])
645 blocks, pruned = prunecontainers(blocks, keep or [])
646 blocks = findsections(blocks)
646 blocks = findsections(blocks)
647 blocks = inlineliterals(blocks)
647 blocks = inlineliterals(blocks)
648 blocks = hgrole(blocks)
648 blocks = hgrole(blocks)
649 blocks = splitparagraphs(blocks)
649 blocks = splitparagraphs(blocks)
650 blocks = updatefieldlists(blocks)
650 blocks = updatefieldlists(blocks)
651 blocks = updateoptionlists(blocks)
651 blocks = updateoptionlists(blocks)
652 blocks = findadmonitions(blocks, admonitions=admonitions)
652 blocks = findadmonitions(blocks, admonitions=admonitions)
653 blocks = addmargins(blocks)
653 blocks = addmargins(blocks)
654 blocks = prunecomments(blocks)
654 blocks = prunecomments(blocks)
655 return blocks, pruned
655 return blocks, pruned
656
656
657 def formatblocks(blocks, width):
657 def formatblocks(blocks, width):
658 text = ''.join(formatblock(b, width) for b in blocks)
658 text = ''.join(formatblock(b, width) for b in blocks)
659 return text
659 return text
660
660
661 def format(text, width=80, indent=0, keep=None, style='plain', section=None):
661 def format(text, width=80, indent=0, keep=None, style='plain', section=None):
662 """Parse and format the text according to width."""
662 """Parse and format the text according to width."""
663 blocks, pruned = parse(text, indent, keep or [])
663 blocks, pruned = parse(text, indent, keep or [])
664 parents = []
664 parents = []
665 if section:
665 if section:
666 sections = getsections(blocks)
666 sections = getsections(blocks)
667 blocks = []
667 blocks = []
668 i = 0
668 i = 0
669 lastparents = []
669 lastparents = []
670 synthetic = []
670 synthetic = []
671 collapse = True
671 collapse = True
672 while i < len(sections):
672 while i < len(sections):
673 name, nest, b = sections[i]
673 name, nest, b = sections[i]
674 del parents[nest:]
674 del parents[nest:]
675 parents.append(i)
675 parents.append(i)
676 if name == section:
676 if name == section:
677 if lastparents != parents:
677 if lastparents != parents:
678 llen = len(lastparents)
678 llen = len(lastparents)
679 plen = len(parents)
679 plen = len(parents)
680 if llen and llen != plen:
680 if llen and llen != plen:
681 collapse = False
681 collapse = False
682 s = []
682 s = []
683 for j in xrange(3, plen - 1):
683 for j in xrange(3, plen - 1):
684 parent = parents[j]
684 parent = parents[j]
685 if (j >= llen or
685 if (j >= llen or
686 lastparents[j] != parent):
686 lastparents[j] != parent):
687 s.append(len(blocks))
687 s.append(len(blocks))
688 sec = sections[parent][2]
688 sec = sections[parent][2]
689 blocks.append(sec[0])
689 blocks.append(sec[0])
690 blocks.append(sec[-1])
690 blocks.append(sec[-1])
691 if s:
691 if s:
692 synthetic.append(s)
692 synthetic.append(s)
693
693
694 lastparents = parents[:]
694 lastparents = parents[:]
695 blocks.extend(b)
695 blocks.extend(b)
696
696
697 ## Also show all subnested sections
697 ## Also show all subnested sections
698 while i + 1 < len(sections) and sections[i + 1][1] > nest:
698 while i + 1 < len(sections) and sections[i + 1][1] > nest:
699 i += 1
699 i += 1
700 blocks.extend(sections[i][2])
700 blocks.extend(sections[i][2])
701 i += 1
701 i += 1
702 if collapse:
702 if collapse:
703 synthetic.reverse()
703 synthetic.reverse()
704 for s in synthetic:
704 for s in synthetic:
705 path = [blocks[syn]['lines'][0] for syn in s]
705 path = [blocks[syn]['lines'][0] for syn in s]
706 real = s[-1] + 2
706 real = s[-1] + 2
707 realline = blocks[real]['lines']
707 realline = blocks[real]['lines']
708 realline[0] = ('"%s"' %
708 realline[0] = ('"%s"' %
709 '.'.join(path + [realline[0]]).replace('"', ''))
709 '.'.join(path + [realline[0]]).replace('"', ''))
710 del blocks[s[0]:real]
710 del blocks[s[0]:real]
711
711
712 if style == 'html':
712 if style == 'html':
713 text = formathtml(blocks)
713 text = formathtml(blocks)
714 else:
714 else:
715 text = ''.join(formatblock(b, width) for b in blocks)
715 text = ''.join(formatblock(b, width) for b in blocks)
716 if keep is None:
716 if keep is None:
717 return text
717 return text
718 else:
718 else:
719 return text, pruned
719 return text, pruned
720
720
721 def getsections(blocks):
721 def getsections(blocks):
722 '''return a list of (section name, nesting level, blocks) tuples'''
722 '''return a list of (section name, nesting level, blocks) tuples'''
723 nest = ""
723 nest = ""
724 level = 0
724 level = 0
725 secs = []
725 secs = []
726
726
727 def getname(b):
727 def getname(b):
728 if b['type'] == 'field':
728 if b['type'] == 'field':
729 x = b['key']
729 x = b['key']
730 else:
730 else:
731 x = b['lines'][0]
731 x = b['lines'][0]
732 x = encoding.lower(x).strip('"')
732 x = encoding.lower(x).strip('"')
733 if '(' in x:
733 if '(' in x:
734 x = x.split('(')[0]
734 x = x.split('(')[0]
735 return x
735 return x
736
736
737 for b in blocks:
737 for b in blocks:
738 if b['type'] == 'section':
738 if b['type'] == 'section':
739 i = b['underline']
739 i = b['underline']
740 if i not in nest:
740 if i not in nest:
741 nest += i
741 nest += i
742 level = nest.index(i) + 1
742 level = nest.index(i) + 1
743 nest = nest[:level]
743 nest = nest[:level]
744 secs.append((getname(b), level, [b]))
744 secs.append((getname(b), level, [b]))
745 elif b['type'] in ('definition', 'field'):
745 elif b['type'] in ('definition', 'field'):
746 i = ' '
746 i = ' '
747 if i not in nest:
747 if i not in nest:
748 nest += i
748 nest += i
749 level = nest.index(i) + 1
749 level = nest.index(i) + 1
750 nest = nest[:level]
750 nest = nest[:level]
751 for i in range(1, len(secs) + 1):
751 for i in range(1, len(secs) + 1):
752 sec = secs[-i]
752 sec = secs[-i]
753 if sec[1] < level:
753 if sec[1] < level:
754 break
754 break
755 siblings = [a for a in sec[2] if a['type'] == 'definition']
755 siblings = [a for a in sec[2] if a['type'] == 'definition']
756 if siblings:
756 if siblings:
757 siblingindent = siblings[-1]['indent']
757 siblingindent = siblings[-1]['indent']
758 indent = b['indent']
758 indent = b['indent']
759 if siblingindent < indent:
759 if siblingindent < indent:
760 level += 1
760 level += 1
761 break
761 break
762 elif siblingindent == indent:
762 elif siblingindent == indent:
763 level = sec[1]
763 level = sec[1]
764 break
764 break
765 secs.append((getname(b), level, [b]))
765 secs.append((getname(b), level, [b]))
766 else:
766 else:
767 if not secs:
767 if not secs:
768 # add an initial empty section
768 # add an initial empty section
769 secs = [('', 0, [])]
769 secs = [('', 0, [])]
770 if b['type'] != 'margin':
770 if b['type'] != 'margin':
771 pointer = 1
771 pointer = 1
772 bindent = b['indent']
772 bindent = b['indent']
773 while pointer < len(secs):
773 while pointer < len(secs):
774 section = secs[-pointer][2][0]
774 section = secs[-pointer][2][0]
775 if section['type'] != 'margin':
775 if section['type'] != 'margin':
776 sindent = section['indent']
776 sindent = section['indent']
777 if len(section['lines']) > 1:
777 if len(section['lines']) > 1:
778 sindent += len(section['lines'][1]) - \
778 sindent += len(section['lines'][1]) - \
779 len(section['lines'][1].lstrip(' '))
779 len(section['lines'][1].lstrip(' '))
780 if bindent >= sindent:
780 if bindent >= sindent:
781 break
781 break
782 pointer += 1
782 pointer += 1
783 if pointer > 1:
783 if pointer > 1:
784 blevel = secs[-pointer][1]
784 blevel = secs[-pointer][1]
785 if section['type'] != b['type']:
785 if section['type'] != b['type']:
786 blevel += 1
786 blevel += 1
787 secs.append(('', blevel, []))
787 secs.append(('', blevel, []))
788 secs[-1][2].append(b)
788 secs[-1][2].append(b)
789 return secs
789 return secs
790
790
791 def decorateblocks(blocks, width):
791 def decorateblocks(blocks, width):
792 '''generate a list of (section name, line text) pairs for search'''
792 '''generate a list of (section name, line text) pairs for search'''
793 lines = []
793 lines = []
794 for s in getsections(blocks):
794 for s in getsections(blocks):
795 section = s[0]
795 section = s[0]
796 text = formatblocks(s[2], width)
796 text = formatblocks(s[2], width)
797 lines.append([(section, l) for l in text.splitlines(True)])
797 lines.append([(section, l) for l in text.splitlines(True)])
798 return lines
798 return lines
799
799
800 def maketable(data, indent=0, header=False):
800 def maketable(data, indent=0, header=False):
801 '''Generate an RST table for the given table data as a list of lines'''
801 '''Generate an RST table for the given table data as a list of lines'''
802
802
803 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
803 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
804 indent = ' ' * indent
804 indent = ' ' * indent
805 div = indent + ' '.join('=' * w for w in widths) + '\n'
805 div = indent + ' '.join('=' * w for w in widths) + '\n'
806
806
807 out = [div]
807 out = [div]
808 for row in data:
808 for row in data:
809 l = []
809 l = []
810 for w, v in zip(widths, row):
810 for w, v in zip(widths, row):
811 if '\n' in v:
811 if '\n' in v:
812 # only remove line breaks and indentation, long lines are
812 # only remove line breaks and indentation, long lines are
813 # handled by the next tool
813 # handled by the next tool
814 v = ' '.join(e.lstrip() for e in v.split('\n'))
814 v = ' '.join(e.lstrip() for e in v.split('\n'))
815 pad = ' ' * (w - encoding.colwidth(v))
815 pad = ' ' * (w - encoding.colwidth(v))
816 l.append(v + pad)
816 l.append(v + pad)
817 out.append(indent + ' '.join(l) + "\n")
817 out.append(indent + ' '.join(l) + "\n")
818 if header and len(data) > 1:
818 if header and len(data) > 1:
819 out.insert(2, div)
819 out.insert(2, div)
820 out.append(div)
820 out.append(div)
821 return out
821 return out
General Comments 0
You need to be logged in to leave comments. Login now