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