minirst.py
677 lines
| 23.5 KiB
| text/x-python
|
PythonLexer
/ mercurial / minirst.py
|
r9156 | # minirst.py - minimal reStructuredText parser | ||
# | ||||
|
r10443 | # Copyright 2009, 2010 Matt Mackall <mpm@selenic.com> and others | ||
|
r9156 | # | ||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r9156 | |||
"""simplified reStructuredText parser. | ||||
This parser knows just enough about reStructuredText to parse the | ||||
Mercurial docstrings. | ||||
It cheats in a major way: nested blocks are not really nested. They | ||||
are just indented blocks that look like they are nested. This relies | ||||
on the user to keep the right indentation for the blocks. | ||||
|
r12958 | Remember to update http://mercurial.selenic.com/wiki/HelpStyleGuide | ||
when adding support for new constructs. | ||||
|
r9156 | """ | ||
|
r15038 | import re | ||
|
r11464 | import util, encoding | ||
|
r12388 | from i18n import _ | ||
|
r11464 | def replace(text, substs): | ||
|
r15393 | ''' | ||
Apply a list of (find, replace) pairs to a text. | ||||
>>> replace("foo bar", [('f', 'F'), ('b', 'B')]) | ||||
'Foo Bar' | ||||
>>> encoding.encoding = 'latin1' | ||||
>>> replace('\\x81\\\\', [('\\\\', '/')]) | ||||
'\\x81/' | ||||
>>> encoding.encoding = 'shiftjis' | ||||
>>> replace('\\x81\\\\', [('\\\\', '/')]) | ||||
'\\x81\\\\' | ||||
''' | ||||
# some character encodings (cp932 for Japanese, at least) use | ||||
# ASCII characters other than control/alphabet/digit as a part of | ||||
# multi-bytes characters, so direct replacing with such characters | ||||
# on strings in local encoding causes invalid byte sequences. | ||||
utext = text.decode(encoding.encoding) | ||||
|
r11464 | for f, t in substs: | ||
|
r15393 | utext = utext.replace(f, t) | ||
return utext.encode(encoding.encoding) | ||||
|
r12651 | |||
_blockre = re.compile(r"\n(?:\s*\n)+") | ||||
|
r9156 | def findblocks(text): | ||
"""Find continuous blocks of lines in text. | ||||
Returns a list of dictionaries representing the blocks. Each block | ||||
has an 'indent' field and a 'lines' field. | ||||
""" | ||||
|
r12651 | blocks = [] | ||
|
r15036 | for b in _blockre.split(text.lstrip('\n').rstrip()): | ||
|
r12651 | lines = b.splitlines() | ||
|
r15123 | if lines: | ||
indent = min((len(l) - len(l.lstrip())) for l in lines) | ||||
lines = [l[indent:] for l in lines] | ||||
blocks.append(dict(indent=indent, lines=lines)) | ||||
|
r9156 | return blocks | ||
def findliteralblocks(blocks): | ||||
"""Finds literal blocks and adds a 'type' field to the blocks. | ||||
Literal blocks are given the type 'literal', all other blocks are | ||||
given type the 'paragraph'. | ||||
""" | ||||
i = 0 | ||||
while i < len(blocks): | ||||
# Searching for a block that looks like this: | ||||
# | ||||
# +------------------------------+ | ||||
# | paragraph | | ||||
# | (ends with "::") | | ||||
# +------------------------------+ | ||||
# +---------------------------+ | ||||
# | indented literal block | | ||||
# +---------------------------+ | ||||
blocks[i]['type'] = 'paragraph' | ||||
|
r10282 | if blocks[i]['lines'][-1].endswith('::') and i + 1 < len(blocks): | ||
|
r9156 | indent = blocks[i]['indent'] | ||
|
r10282 | adjustment = blocks[i + 1]['indent'] - indent | ||
|
r9156 | |||
if blocks[i]['lines'] == ['::']: | ||||
# Expanded form: remove block | ||||
del blocks[i] | ||||
i -= 1 | ||||
elif blocks[i]['lines'][-1].endswith(' ::'): | ||||
# Partially minimized form: remove space and both | ||||
# colons. | ||||
blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-3] | ||||
else: | ||||
# Fully minimized form: remove just one colon. | ||||
blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-1] | ||||
# List items are formatted with a hanging indent. We must | ||||
# correct for this here while we still have the original | ||||
# information on the indentation of the subsequent literal | ||||
# blocks available. | ||||
|
r9738 | m = _bulletre.match(blocks[i]['lines'][0]) | ||
if m: | ||||
indent += m.end() | ||||
adjustment -= m.end() | ||||
|
r9156 | |||
# Mark the following indented blocks. | ||||
|
r10282 | while i + 1 < len(blocks) and blocks[i + 1]['indent'] > indent: | ||
blocks[i + 1]['type'] = 'literal' | ||||
blocks[i + 1]['indent'] -= adjustment | ||||
|
r9156 | i += 1 | ||
i += 1 | ||||
return blocks | ||||
|
r10447 | _bulletre = re.compile(r'(-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ') | ||
|
r13011 | _optionre = re.compile(r'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)' | ||
r'((.*) +)(.*)$') | ||||
|
r10065 | _fieldre = re.compile(r':(?![: ])([^:]*)(?<! ):[ ]+(.*)') | ||
|
r9737 | _definitionre = re.compile(r'[^ ]') | ||
|
r15037 | _tablere = re.compile(r'(=+\s+)*=+') | ||
|
r9737 | |||
def splitparagraphs(blocks): | ||||
"""Split paragraphs into lists.""" | ||||
# Tuples with (list type, item regexp, single line items?). Order | ||||
# matters: definition lists has the least specific regexp and must | ||||
# come last. | ||||
listtypes = [('bullet', _bulletre, True), | ||||
('option', _optionre, True), | ||||
('field', _fieldre, True), | ||||
('definition', _definitionre, False)] | ||||
def match(lines, i, itemre, singleline): | ||||
"""Does itemre match an item at line i? | ||||
A list item can be followed by an idented line or another list | ||||
item (but only if singleline is True). | ||||
""" | ||||
line1 = lines[i] | ||||
|
r10282 | line2 = i + 1 < len(lines) and lines[i + 1] or '' | ||
|
r9737 | if not itemre.match(line1): | ||
return False | ||||
if singleline: | ||||
return line2 == '' or line2[0] == ' ' or itemre.match(line2) | ||||
else: | ||||
return line2.startswith(' ') | ||||
i = 0 | ||||
while i < len(blocks): | ||||
if blocks[i]['type'] == 'paragraph': | ||||
lines = blocks[i]['lines'] | ||||
for type, itemre, singleline in listtypes: | ||||
if match(lines, 0, itemre, singleline): | ||||
items = [] | ||||
for j, line in enumerate(lines): | ||||
if match(lines, j, itemre, singleline): | ||||
items.append(dict(type=type, lines=[], | ||||
indent=blocks[i]['indent'])) | ||||
items[-1]['lines'].append(line) | ||||
|
r10282 | blocks[i:i + 1] = items | ||
|
r9737 | break | ||
i += 1 | ||||
return blocks | ||||
|
r15861 | _fieldwidth = 14 | ||
|
r10065 | |||
def updatefieldlists(blocks): | ||||
|
r15861 | """Find key for field lists.""" | ||
|
r10065 | i = 0 | ||
while i < len(blocks): | ||||
if blocks[i]['type'] != 'field': | ||||
i += 1 | ||||
continue | ||||
j = i | ||||
while j < len(blocks) and blocks[j]['type'] == 'field': | ||||
m = _fieldre.match(blocks[j]['lines'][0]) | ||||
key, rest = m.groups() | ||||
blocks[j]['lines'][0] = rest | ||||
blocks[j]['key'] = key | ||||
j += 1 | ||||
i = j + 1 | ||||
return blocks | ||||
|
r13011 | def updateoptionlists(blocks): | ||
i = 0 | ||||
while i < len(blocks): | ||||
if blocks[i]['type'] != 'option': | ||||
i += 1 | ||||
continue | ||||
optstrwidth = 0 | ||||
j = i | ||||
while j < len(blocks) and blocks[j]['type'] == 'option': | ||||
m = _optionre.match(blocks[j]['lines'][0]) | ||||
shortoption = m.group(2) | ||||
group3 = m.group(3) | ||||
longoption = group3[2:].strip() | ||||
desc = m.group(6).strip() | ||||
longoptionarg = m.group(5).strip() | ||||
blocks[j]['lines'][0] = desc | ||||
noshortop = '' | ||||
if not shortoption: | ||||
noshortop = ' ' | ||||
opt = "%s%s" % (shortoption and "-%s " % shortoption or '', | ||||
("%s--%s %s") % (noshortop, longoption, | ||||
longoptionarg)) | ||||
opt = opt.rstrip() | ||||
blocks[j]['optstr'] = opt | ||||
optstrwidth = max(optstrwidth, encoding.colwidth(opt)) | ||||
j += 1 | ||||
for block in blocks[i:j]: | ||||
block['optstrwidth'] = optstrwidth | ||||
i = j + 1 | ||||
return blocks | ||||
|
r10443 | def prunecontainers(blocks, keep): | ||
"""Prune unwanted containers. | ||||
The blocks must have a 'type' field, i.e., they should have been | ||||
run through findliteralblocks first. | ||||
""" | ||||
|
r10444 | pruned = [] | ||
|
r10443 | i = 0 | ||
while i + 1 < len(blocks): | ||||
# Searching for a block that looks like this: | ||||
# | ||||
# +-------+---------------------------+ | ||||
# | ".. container ::" type | | ||||
# +---+ | | ||||
# | blocks | | ||||
# +-------------------------------+ | ||||
if (blocks[i]['type'] == 'paragraph' and | ||||
blocks[i]['lines'][0].startswith('.. container::')): | ||||
indent = blocks[i]['indent'] | ||||
adjustment = blocks[i + 1]['indent'] - indent | ||||
containertype = blocks[i]['lines'][0][15:] | ||||
prune = containertype not in keep | ||||
|
r10444 | if prune: | ||
pruned.append(containertype) | ||||
|
r10443 | |||
# Always delete "..container:: type" block | ||||
del blocks[i] | ||||
j = i | ||||
|
r15102 | i -= 1 | ||
|
r10443 | while j < len(blocks) and blocks[j]['indent'] > indent: | ||
if prune: | ||||
del blocks[j] | ||||
else: | ||||
blocks[j]['indent'] -= adjustment | ||||
j += 1 | ||||
i += 1 | ||||
|
r10444 | return blocks, pruned | ||
|
r10443 | |||
|
r10984 | _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""") | ||
|
r15037 | def findtables(blocks): | ||
'''Find simple tables | ||||
Only simple one-line table elements are supported | ||||
''' | ||||
for block in blocks: | ||||
# Searching for a block that looks like this: | ||||
# | ||||
# === ==== === | ||||
# A B C | ||||
# === ==== === <- optional | ||||
# 1 2 3 | ||||
# x y z | ||||
# === ==== === | ||||
if (block['type'] == 'paragraph' and | ||||
|
r15192 | len(block['lines']) > 2 and | ||
|
r15037 | _tablere.match(block['lines'][0]) and | ||
block['lines'][0] == block['lines'][-1]): | ||||
block['type'] = 'table' | ||||
block['header'] = False | ||||
div = block['lines'][0] | ||||
|
r15144 | |||
# column markers are ASCII so we can calculate column | ||||
# position in bytes | ||||
|
r15037 | columns = [x for x in xrange(len(div)) | ||
if div[x] == '=' and (x == 0 or div[x - 1] == ' ')] | ||||
rows = [] | ||||
for l in block['lines'][1:-1]: | ||||
if l == div: | ||||
block['header'] = True | ||||
continue | ||||
row = [] | ||||
|
r15144 | # we measure columns not in bytes or characters but in | ||
# colwidth which makes things tricky | ||||
pos = columns[0] # leading whitespace is bytes | ||||
|
r15037 | for n, start in enumerate(columns): | ||
if n + 1 < len(columns): | ||||
|
r15144 | width = columns[n + 1] - start | ||
v = encoding.getcols(l, pos, width) # gather columns | ||||
pos += len(v) # calculate byte position of end | ||||
row.append(v.strip()) | ||||
|
r15037 | else: | ||
|
r15144 | row.append(l[pos:].strip()) | ||
|
r15037 | rows.append(row) | ||
|
r15144 | |||
|
r15037 | block['table'] = rows | ||
return blocks | ||||
|
r9156 | def findsections(blocks): | ||
"""Finds sections. | ||||
The blocks must have a 'type' field, i.e., they should have been | ||||
run through findliteralblocks first. | ||||
""" | ||||
for block in blocks: | ||||
# Searching for a block that looks like this: | ||||
# | ||||
# +------------------------------+ | ||||
# | Section title | | ||||
# | ------------- | | ||||
# +------------------------------+ | ||||
if (block['type'] == 'paragraph' and | ||||
len(block['lines']) == 2 and | ||||
|
r12867 | encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and | ||
|
r10984 | _sectionre.match(block['lines'][1])): | ||
|
r10983 | block['underline'] = block['lines'][1][0] | ||
|
r9156 | block['type'] = 'section' | ||
|
r10983 | del block['lines'][1] | ||
|
r9156 | return blocks | ||
|
r9623 | def inlineliterals(blocks): | ||
|
r11464 | substs = [('``', '"')] | ||
|
r9623 | for b in blocks: | ||
|
r10983 | if b['type'] in ('paragraph', 'section'): | ||
|
r11464 | b['lines'] = [replace(l, substs) for l in b['lines']] | ||
|
r9623 | return blocks | ||
|
r10972 | def hgrole(blocks): | ||
|
r11464 | substs = [(':hg:`', '"hg '), ('`', '"')] | ||
|
r10972 | for b in blocks: | ||
|
r10983 | if b['type'] in ('paragraph', 'section'): | ||
|
r11192 | # Turn :hg:`command` into "hg command". This also works | ||
# when there is a line break in the command and relies on | ||||
# the fact that we have no stray back-quotes in the input | ||||
# (run the blocks through inlineliterals first). | ||||
|
r11464 | b['lines'] = [replace(l, substs) for l in b['lines']] | ||
|
r10972 | return blocks | ||
|
r9156 | def addmargins(blocks): | ||
"""Adds empty blocks for vertical spacing. | ||||
This groups bullets, options, and definitions together with no vertical | ||||
space between them, and adds an empty block between all other blocks. | ||||
""" | ||||
i = 1 | ||||
while i < len(blocks): | ||||
|
r10282 | if (blocks[i]['type'] == blocks[i - 1]['type'] and | ||
|
r10936 | blocks[i]['type'] in ('bullet', 'option', 'field')): | ||
|
r9156 | i += 1 | ||
else: | ||||
blocks.insert(i, dict(lines=[''], indent=0, type='margin')) | ||||
i += 2 | ||||
return blocks | ||||
|
r12819 | def prunecomments(blocks): | ||
"""Remove comments.""" | ||||
i = 0 | ||||
while i < len(blocks): | ||||
b = blocks[i] | ||||
|
r13009 | if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or | ||
b['lines'] == ['..']): | ||||
|
r12819 | del blocks[i] | ||
|
r13003 | if i < len(blocks) and blocks[i]['type'] == 'margin': | ||
del blocks[i] | ||||
|
r12819 | else: | ||
i += 1 | ||||
return blocks | ||||
|
r12620 | _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|" | ||
r"error|hint|important|note|tip|warning)::", | ||||
flags=re.IGNORECASE) | ||||
|
r12388 | def findadmonitions(blocks): | ||
""" | ||||
Makes the type of the block an admonition block if | ||||
the first line is an admonition directive | ||||
""" | ||||
i = 0 | ||||
while i < len(blocks): | ||||
|
r12620 | m = _admonitionre.match(blocks[i]['lines'][0]) | ||
|
r12388 | if m: | ||
blocks[i]['type'] = 'admonition' | ||||
admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower() | ||||
firstline = blocks[i]['lines'][0][m.end() + 1:] | ||||
|
r12620 | if firstline: | ||
blocks[i]['lines'].insert(1, ' ' + firstline) | ||||
|
r12388 | |||
blocks[i]['admonitiontitle'] = admonitiontitle | ||||
del blocks[i]['lines'][0] | ||||
i = i + 1 | ||||
return blocks | ||||
|
r9156 | |||
|
r12652 | _admonitiontitles = {'attention': _('Attention:'), | ||
'caution': _('Caution:'), | ||||
'danger': _('!Danger!') , | ||||
'error': _('Error:'), | ||||
'hint': _('Hint:'), | ||||
'important': _('Important:'), | ||||
'note': _('Note:'), | ||||
'tip': _('Tip:'), | ||||
'warning': _('Warning!')} | ||||
|
r13011 | def formatoption(block, width): | ||
desc = ' '.join(map(str.strip, block['lines'])) | ||||
colwidth = encoding.colwidth(block['optstr']) | ||||
usablewidth = width - 1 | ||||
hanging = block['optstrwidth'] | ||||
initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth))) | ||||
hangindent = ' ' * (encoding.colwidth(initindent) + 1) | ||||
|
r15125 | return ' %s\n' % (util.wrap(desc, usablewidth, | ||
|
r13011 | initindent=initindent, | ||
hangindent=hangindent)) | ||||
|
r9156 | def formatblock(block, width): | ||
"""Format a block according to width.""" | ||||
|
r9417 | if width <= 0: | ||
width = 78 | ||||
|
r9156 | indent = ' ' * block['indent'] | ||
|
r12388 | if block['type'] == 'admonition': | ||
|
r12652 | admonition = _admonitiontitles[block['admonitiontitle']] | ||
|
r12388 | hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip()) | ||
defindent = indent + hang * ' ' | ||||
text = ' '.join(map(str.strip, block['lines'])) | ||||
|
r15125 | return '%s\n%s\n' % (indent + admonition, | ||
util.wrap(text, width=width, | ||||
initindent=defindent, | ||||
hangindent=defindent)) | ||||
|
r9156 | if block['type'] == 'margin': | ||
|
r15125 | return '\n' | ||
|
r9735 | if block['type'] == 'literal': | ||
|
r9291 | indent += ' ' | ||
|
r15125 | return indent + ('\n' + indent).join(block['lines']) + '\n' | ||
|
r9735 | if block['type'] == 'section': | ||
|
r12867 | underline = encoding.colwidth(block['lines'][0]) * block['underline'] | ||
|
r15125 | return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline) | ||
|
r15037 | if block['type'] == 'table': | ||
table = block['table'] | ||||
# compute column widths | ||||
widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)] | ||||
text = '' | ||||
span = sum(widths) + len(widths) - 1 | ||||
indent = ' ' * block['indent'] | ||||
hang = ' ' * (len(indent) + span - widths[-1]) | ||||
for row in table: | ||||
|
r15144 | l = [] | ||
for w, v in zip(widths, row): | ||||
pad = ' ' * (w - encoding.colwidth(v)) | ||||
l.append(v + pad) | ||||
l = ' '.join(l) | ||||
|
r15037 | l = util.wrap(l, width=width, initindent=indent, hangindent=hang) | ||
if not text and block['header']: | ||||
text = l + '\n' + indent + '-' * (min(width, span)) + '\n' | ||||
else: | ||||
text += l + "\n" | ||||
return text | ||||
|
r9735 | if block['type'] == 'definition': | ||
|
r9156 | term = indent + block['lines'][0] | ||
|
r9737 | hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip()) | ||
defindent = indent + hang * ' ' | ||||
|
r9156 | text = ' '.join(map(str.strip, block['lines'][1:])) | ||
|
r15125 | return '%s\n%s\n' % (term, util.wrap(text, width=width, | ||
initindent=defindent, | ||||
hangindent=defindent)) | ||||
|
r10937 | subindent = indent | ||
|
r9735 | if block['type'] == 'bullet': | ||
|
r10447 | if block['lines'][0].startswith('| '): | ||
# Remove bullet for line blocks and add no extra | ||||
# indention. | ||||
block['lines'][0] = block['lines'][0][2:] | ||||
else: | ||||
m = _bulletre.match(block['lines'][0]) | ||||
subindent = indent + m.end() * ' ' | ||||
|
r9737 | elif block['type'] == 'field': | ||
|
r10065 | key = block['key'] | ||
subindent = indent + _fieldwidth * ' ' | ||||
if len(key) + 2 > _fieldwidth: | ||||
# key too large, use full line width | ||||
key = key.ljust(width) | ||||
else: | ||||
|
r15861 | # key fits within field width | ||
|
r10065 | key = key.ljust(_fieldwidth) | ||
block['lines'][0] = key + block['lines'][0] | ||||
|
r9737 | elif block['type'] == 'option': | ||
|
r13011 | return formatoption(block, width) | ||
|
r9156 | |||
|
r9737 | text = ' '.join(map(str.strip, block['lines'])) | ||
|
r11297 | return util.wrap(text, width=width, | ||
initindent=indent, | ||||
|
r15125 | hangindent=subindent) + '\n' | ||
|
r9156 | |||
|
r15261 | def formathtml(blocks): | ||
"""Format RST blocks as HTML""" | ||||
out = [] | ||||
headernest = '' | ||||
listnest = [] | ||||
def openlist(start, level): | ||||
if not listnest or listnest[-1][0] != start: | ||||
listnest.append((start, level)) | ||||
out.append('<%s>\n' % start) | ||||
blocks = [b for b in blocks if b['type'] != 'margin'] | ||||
for pos, b in enumerate(blocks): | ||||
btype = b['type'] | ||||
level = b['indent'] | ||||
lines = b['lines'] | ||||
if btype == 'admonition': | ||||
admonition = _admonitiontitles[b['admonitiontitle']] | ||||
text = ' '.join(map(str.strip, lines)) | ||||
out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text)) | ||||
elif btype == 'paragraph': | ||||
out.append('<p>\n%s\n</p>\n' % '\n'.join(lines)) | ||||
elif btype == 'margin': | ||||
pass | ||||
elif btype == 'literal': | ||||
out.append('<pre>\n%s\n</pre>\n' % '\n'.join(lines)) | ||||
elif btype == 'section': | ||||
i = b['underline'] | ||||
if i not in headernest: | ||||
headernest += i | ||||
level = headernest.index(i) + 1 | ||||
out.append('<h%d>%s</h%d>\n' % (level, lines[0], level)) | ||||
elif btype == 'table': | ||||
table = b['table'] | ||||
t = [] | ||||
for row in table: | ||||
l = [] | ||||
for v in zip(row): | ||||
if not t: | ||||
l.append('<th>%s</th>' % v) | ||||
else: | ||||
l.append('<td>%s</td>' % v) | ||||
t.append(' <tr>%s</tr>\n' % ''.join(l)) | ||||
out.append('<table>\n%s</table>\n' % ''.join(t)) | ||||
elif btype == 'definition': | ||||
openlist('dl', level) | ||||
term = lines[0] | ||||
text = ' '.join(map(str.strip, lines[1:])) | ||||
out.append(' <dt>%s\n <dd>%s\n' % (term, text)) | ||||
elif btype == 'bullet': | ||||
bullet, head = lines[0].split(' ', 1) | ||||
if bullet == '-': | ||||
openlist('ul', level) | ||||
else: | ||||
openlist('ol', level) | ||||
out.append(' <li> %s\n' % ' '.join([head] + lines[1:])) | ||||
elif btype == 'field': | ||||
openlist('dl', level) | ||||
key = b['key'] | ||||
text = ' '.join(map(str.strip, lines)) | ||||
out.append(' <dt>%s\n <dd>%s\n' % (key, text)) | ||||
elif btype == 'option': | ||||
openlist('dl', level) | ||||
opt = b['optstr'] | ||||
desc = ' '.join(map(str.strip, lines)) | ||||
out.append(' <dt>%s\n <dd>%s\n' % (opt, desc)) | ||||
# close lists if indent level of next block is lower | ||||
if listnest: | ||||
start, level = listnest[-1] | ||||
if pos == len(blocks) - 1: | ||||
out.append('</%s>\n' % start) | ||||
listnest.pop() | ||||
else: | ||||
nb = blocks[pos + 1] | ||||
ni = nb['indent'] | ||||
if (ni < level or | ||||
(ni == level and | ||||
nb['type'] not in 'definition bullet field option')): | ||||
out.append('</%s>\n' % start) | ||||
listnest.pop() | ||||
return ''.join(out) | ||||
|
r15012 | def parse(text, indent=0, keep=None): | ||
"""Parse text into a list of blocks""" | ||||
pruned = [] | ||||
|
r9156 | blocks = findblocks(text) | ||
|
r9540 | for b in blocks: | ||
b['indent'] += indent | ||||
|
r9156 | blocks = findliteralblocks(blocks) | ||
|
r15037 | blocks = findtables(blocks) | ||
|
r10444 | blocks, pruned = prunecontainers(blocks, keep or []) | ||
|
r10983 | blocks = findsections(blocks) | ||
|
r9623 | blocks = inlineliterals(blocks) | ||
|
r10972 | blocks = hgrole(blocks) | ||
|
r9737 | blocks = splitparagraphs(blocks) | ||
|
r10065 | blocks = updatefieldlists(blocks) | ||
|
r13011 | blocks = updateoptionlists(blocks) | ||
|
r13003 | blocks = addmargins(blocks) | ||
|
r12819 | blocks = prunecomments(blocks) | ||
|
r12388 | blocks = findadmonitions(blocks) | ||
|
r15012 | return blocks, pruned | ||
|
r15013 | def formatblocks(blocks, width): | ||
|
r15125 | text = ''.join(formatblock(b, width) for b in blocks) | ||
|
r15013 | return text | ||
|
r15262 | def format(text, width=80, indent=0, keep=None, style='plain'): | ||
|
r15012 | """Parse and format the text according to width.""" | ||
blocks, pruned = parse(text, indent, keep or []) | ||||
|
r15262 | if style == 'html': | ||
text = formathtml(blocks) | ||||
else: | ||||
text = ''.join(formatblock(b, width) for b in blocks) | ||||
|
r10444 | if keep is None: | ||
return text | ||||
else: | ||||
return text, pruned | ||||
|
r9156 | |||
|
r15014 | def getsections(blocks): | ||
'''return a list of (section name, nesting level, blocks) tuples''' | ||||
nest = "" | ||||
level = 0 | ||||
secs = [] | ||||
for b in blocks: | ||||
if b['type'] == 'section': | ||||
i = b['underline'] | ||||
if i not in nest: | ||||
nest += i | ||||
level = nest.index(i) + 1 | ||||
nest = nest[:level] | ||||
secs.append((b['lines'][0], level, [b])) | ||||
else: | ||||
if not secs: | ||||
# add an initial empty section | ||||
secs = [('', 0, [])] | ||||
secs[-1][2].append(b) | ||||
return secs | ||||
|
r9156 | |||
|
r15015 | def decorateblocks(blocks, width): | ||
'''generate a list of (section name, line text) pairs for search''' | ||||
lines = [] | ||||
for s in getsections(blocks): | ||||
section = s[0] | ||||
text = formatblocks(s[2], width) | ||||
lines.append([(section, l) for l in text.splitlines(True)]) | ||||
return lines | ||||
|
r15039 | |||
def maketable(data, indent=0, header=False): | ||||
|
r16815 | '''Generate an RST table for the given table data as a list of lines''' | ||
|
r15039 | |||
widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)] | ||||
indent = ' ' * indent | ||||
div = indent + ' '.join('=' * w for w in widths) + '\n' | ||||
out = [div] | ||||
for row in data: | ||||
|
r15144 | l = [] | ||
for w, v in zip(widths, row): | ||||
pad = ' ' * (w - encoding.colwidth(v)) | ||||
l.append(v + pad) | ||||
out.append(indent + ' '.join(l) + "\n") | ||||
|
r15039 | if header and len(data) > 1: | ||
out.insert(2, div) | ||||
out.append(div) | ||||
|
r16815 | return out | ||