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