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