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