minirst.py
900 lines
| 29.1 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. | ||||
|
r26421 | Remember to update https://mercurial-scm.org/wiki/HelpStyleGuide | ||
|
r12958 | when adding support for new constructs. | ||
|
r9156 | """ | ||
|
r25960 | from __future__ import absolute_import | ||
|
r12388 | |||
|
r25960 | import re | ||
from .i18n import _ | ||||
from . import ( | ||||
encoding, | ||||
|
r31318 | pycompat, | ||
|
r34696 | url, | ||
|
r37102 | ) | ||
|
r43346 | from .utils import stringutil | ||
|
r18750 | |||
|
r18748 | def section(s): | ||
|
r43347 | return b"%s\n%s\n\n" % (s, b"\"" * encoding.colwidth(s)) | ||
|
r18748 | |||
|
r43346 | |||
|
r18748 | def subsection(s): | ||
|
r43347 | return b"%s\n%s\n\n" % (s, b'=' * encoding.colwidth(s)) | ||
|
r18748 | |||
|
r43346 | |||
|
r18748 | def subsubsection(s): | ||
|
r43347 | return b"%s\n%s\n\n" % (s, b"-" * encoding.colwidth(s)) | ||
|
r18748 | |||
|
r43346 | |||
|
r18748 | def subsubsubsection(s): | ||
|
r43347 | return b"%s\n%s\n\n" % (s, b"." * encoding.colwidth(s)) | ||
|
r18748 | |||
|
r43346 | |||
|
r42437 | def subsubsubsubsection(s): | ||
|
r43347 | return b"%s\n%s\n\n" % (s, b"'" * encoding.colwidth(s)) | ||
|
r42437 | |||
|
r43346 | |||
|
r11464 | def replace(text, substs): | ||
|
r46554 | """ | ||
|
r15393 | Apply a list of (find, replace) pairs to a text. | ||
|
r34133 | >>> replace(b"foo bar", [(b'f', b'F'), (b'b', b'B')]) | ||
|
r15393 | 'Foo Bar' | ||
|
r34133 | >>> encoding.encoding = b'latin1' | ||
>>> replace(b'\\x81\\\\', [(b'\\\\', b'/')]) | ||||
|
r15393 | '\\x81/' | ||
|
r34133 | >>> encoding.encoding = b'shiftjis' | ||
>>> replace(b'\\x81\\\\', [(b'\\\\', b'/')]) | ||||
|
r15393 | '\\x81\\\\' | ||
|
r46554 | """ | ||
|
r15393 | |||
# 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. | ||||
|
r31318 | utext = text.decode(pycompat.sysstr(encoding.encoding)) | ||
|
r11464 | for f, t in substs: | ||
|
r21745 | utext = utext.replace(f.decode("ascii"), t.decode("ascii")) | ||
|
r31318 | return utext.encode(pycompat.sysstr(encoding.encoding)) | ||
|
r12651 | |||
|
r43346 | |||
|
r31317 | _blockre = re.compile(br"\n(?:\s*\n)+") | ||
|
r12651 | |||
|
r43346 | |||
|
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 = [] | ||
|
r43347 | for b in _blockre.split(text.lstrip(b'\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] | ||||
|
r43347 | blocks.append({b'indent': indent, b'lines': lines}) | ||
|
r9156 | return blocks | ||
|
r43346 | |||
|
r9156 | 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 | | ||||
# +---------------------------+ | ||||
|
r43347 | blocks[i][b'type'] = b'paragraph' | ||
if blocks[i][b'lines'][-1].endswith(b'::') and i + 1 < len(blocks): | ||||
indent = blocks[i][b'indent'] | ||||
adjustment = blocks[i + 1][b'indent'] - indent | ||||
|
r9156 | |||
|
r43347 | if blocks[i][b'lines'] == [b'::']: | ||
|
r9156 | # Expanded form: remove block | ||
del blocks[i] | ||||
i -= 1 | ||||
|
r43347 | elif blocks[i][b'lines'][-1].endswith(b' ::'): | ||
|
r9156 | # Partially minimized form: remove space and both | ||
# colons. | ||||
|
r43347 | blocks[i][b'lines'][-1] = blocks[i][b'lines'][-1][:-3] | ||
|
r43346 | elif ( | ||
|
r43347 | len(blocks[i][b'lines']) == 1 | ||
and blocks[i][b'lines'][0].lstrip(b' ').startswith(b'.. ') | ||||
and blocks[i][b'lines'][0].find(b' ', 3) == -1 | ||||
|
r43346 | ): | ||
|
r20549 | # directive on its own line, not a literal block | ||
|
r19992 | i += 1 | ||
continue | ||||
|
r9156 | else: | ||
# Fully minimized form: remove just one colon. | ||||
|
r43347 | blocks[i][b'lines'][-1] = blocks[i][b'lines'][-1][:-1] | ||
|
r9156 | |||
# 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. | ||||
|
r43347 | m = _bulletre.match(blocks[i][b'lines'][0]) | ||
|
r9738 | if m: | ||
indent += m.end() | ||||
adjustment -= m.end() | ||||
|
r9156 | |||
# Mark the following indented blocks. | ||||
|
r43347 | while i + 1 < len(blocks) and blocks[i + 1][b'indent'] > indent: | ||
blocks[i + 1][b'type'] = b'literal' | ||||
blocks[i + 1][b'indent'] -= adjustment | ||||
|
r9156 | i += 1 | ||
i += 1 | ||||
return blocks | ||||
|
r43346 | |||
|
r31317 | _bulletre = re.compile(br'(\*|-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ') | ||
|
r43346 | _optionre = re.compile( | ||
br'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)' br'((.*) +)(.*)$' | ||||
) | ||||
|
r31317 | _fieldre = re.compile(br':(?![: ])([^:]*)(?<! ):[ ]+(.*)') | ||
_definitionre = re.compile(br'[^ ]') | ||||
_tablere = re.compile(br'(=+\s+)*=+') | ||||
|
r9737 | |||
|
r43346 | |||
|
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. | ||||
|
r43346 | listtypes = [ | ||
|
r43347 | (b'bullet', _bulletre, True), | ||
(b'option', _optionre, True), | ||||
(b'field', _fieldre, True), | ||||
(b'definition', _definitionre, False), | ||||
|
r43346 | ] | ||
|
r9737 | |||
def match(lines, i, itemre, singleline): | ||||
"""Does itemre match an item at line i? | ||||
|
r17424 | A list item can be followed by an indented line or another list | ||
|
r9737 | item (but only if singleline is True). | ||
""" | ||||
line1 = lines[i] | ||||
|
r43347 | line2 = i + 1 < len(lines) and lines[i + 1] or b'' | ||
|
r9737 | if not itemre.match(line1): | ||
return False | ||||
if singleline: | ||||
|
r43347 | return line2 == b'' or line2[0:1] == b' ' or itemre.match(line2) | ||
|
r9737 | else: | ||
|
r43347 | return line2.startswith(b' ') | ||
|
r9737 | |||
i = 0 | ||||
while i < len(blocks): | ||||
|
r43347 | if blocks[i][b'type'] == b'paragraph': | ||
lines = blocks[i][b'lines'] | ||||
|
r9737 | 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): | ||||
|
r43346 | items.append( | ||
{ | ||||
|
r43347 | b'type': type, | ||
b'lines': [], | ||||
b'indent': blocks[i][b'indent'], | ||||
|
r43346 | } | ||
) | ||||
|
r43347 | items[-1][b'lines'].append(line) | ||
|
r43346 | blocks[i : i + 1] = items | ||
|
r9737 | break | ||
i += 1 | ||||
return blocks | ||||
|
r43346 | |||
|
r15861 | _fieldwidth = 14 | ||
|
r10065 | |||
|
r43346 | |||
|
r10065 | def updatefieldlists(blocks): | ||
|
r15861 | """Find key for field lists.""" | ||
|
r10065 | i = 0 | ||
while i < len(blocks): | ||||
|
r43347 | if blocks[i][b'type'] != b'field': | ||
|
r10065 | i += 1 | ||
continue | ||||
j = i | ||||
|
r43347 | while j < len(blocks) and blocks[j][b'type'] == b'field': | ||
m = _fieldre.match(blocks[j][b'lines'][0]) | ||||
|
r10065 | key, rest = m.groups() | ||
|
r43347 | blocks[j][b'lines'][0] = rest | ||
blocks[j][b'key'] = key | ||||
|
r10065 | j += 1 | ||
i = j + 1 | ||||
return blocks | ||||
|
r43346 | |||
|
r13011 | def updateoptionlists(blocks): | ||
i = 0 | ||||
while i < len(blocks): | ||||
|
r43347 | if blocks[i][b'type'] != b'option': | ||
|
r13011 | i += 1 | ||
continue | ||||
optstrwidth = 0 | ||||
j = i | ||||
|
r43347 | while j < len(blocks) and blocks[j][b'type'] == b'option': | ||
m = _optionre.match(blocks[j][b'lines'][0]) | ||||
|
r13011 | |||
shortoption = m.group(2) | ||||
group3 = m.group(3) | ||||
longoption = group3[2:].strip() | ||||
desc = m.group(6).strip() | ||||
longoptionarg = m.group(5).strip() | ||||
|
r43347 | blocks[j][b'lines'][0] = desc | ||
|
r13011 | |||
|
r43347 | noshortop = b'' | ||
|
r13011 | if not shortoption: | ||
|
r43347 | noshortop = b' ' | ||
|
r13011 | |||
|
r43347 | opt = b"%s%s" % ( | ||
shortoption and b"-%s " % shortoption or b'', | ||||
b"%s--%s %s" % (noshortop, longoption, longoptionarg), | ||||
|
r43346 | ) | ||
|
r13011 | opt = opt.rstrip() | ||
|
r43347 | blocks[j][b'optstr'] = opt | ||
|
r13011 | optstrwidth = max(optstrwidth, encoding.colwidth(opt)) | ||
j += 1 | ||||
for block in blocks[i:j]: | ||||
|
r43347 | block[b'optstrwidth'] = optstrwidth | ||
|
r13011 | i = j + 1 | ||
return blocks | ||||
|
r43346 | |||
|
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 | | ||||
# +-------------------------------+ | ||||
|
r43347 | if blocks[i][b'type'] == b'paragraph' and blocks[i][b'lines'][ | ||
|
r43346 | 0 | ||
|
r43347 | ].startswith(b'.. container::'): | ||
indent = blocks[i][b'indent'] | ||||
adjustment = blocks[i + 1][b'indent'] - indent | ||||
containertype = blocks[i][b'lines'][0][15:] | ||||
|
r22584 | prune = True | ||
for c in keep: | ||||
|
r43347 | if c in containertype.split(b'.'): | ||
|
r22584 | prune = False | ||
|
r10444 | if prune: | ||
pruned.append(containertype) | ||||
|
r10443 | |||
# Always delete "..container:: type" block | ||||
del blocks[i] | ||||
j = i | ||||
|
r15102 | i -= 1 | ||
|
r43347 | while j < len(blocks) and blocks[j][b'indent'] > indent: | ||
|
r10443 | if prune: | ||
del blocks[j] | ||||
else: | ||||
|
r43347 | blocks[j][b'indent'] -= adjustment | ||
|
r10443 | j += 1 | ||
i += 1 | ||||
|
r10444 | return blocks, pruned | ||
|
r10443 | |||
|
r43346 | |||
|
r31317 | _sectionre = re.compile(br"""^([-=`:.'"~^_*+#])\1+$""") | ||
|
r10984 | |||
|
r43346 | |||
|
r15037 | def findtables(blocks): | ||
|
r46554 | """Find simple tables | ||
|
r15037 | |||
|
r46554 | Only simple one-line table elements are supported | ||
""" | ||||
|
r15037 | |||
for block in blocks: | ||||
# Searching for a block that looks like this: | ||||
# | ||||
# === ==== === | ||||
# A B C | ||||
# === ==== === <- optional | ||||
# 1 2 3 | ||||
# x y z | ||||
# === ==== === | ||||
|
r43346 | if ( | ||
|
r43347 | block[b'type'] == b'paragraph' | ||
and len(block[b'lines']) > 2 | ||||
and _tablere.match(block[b'lines'][0]) | ||||
and block[b'lines'][0] == block[b'lines'][-1] | ||||
|
r43346 | ): | ||
|
r43347 | block[b'type'] = b'table' | ||
block[b'header'] = False | ||||
div = block[b'lines'][0] | ||||
|
r15144 | |||
# column markers are ASCII so we can calculate column | ||||
# position in bytes | ||||
|
r43346 | columns = [ | ||
x | ||||
for x in pycompat.xrange(len(div)) | ||||
|
r43347 | if div[x : x + 1] == b'=' and (x == 0 or div[x - 1 : x] == b' ') | ||
|
r43346 | ] | ||
|
r15037 | rows = [] | ||
|
r43347 | for l in block[b'lines'][1:-1]: | ||
|
r15037 | if l == div: | ||
|
r43347 | block[b'header'] = True | ||
|
r15037 | continue | ||
row = [] | ||||
|
r15144 | # we measure columns not in bytes or characters but in | ||
# colwidth which makes things tricky | ||||
|
r43346 | 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 | ||
|
r43346 | v = encoding.getcols(l, pos, width) # gather columns | ||
pos += len(v) # calculate byte position of end | ||||
|
r15144 | row.append(v.strip()) | ||
|
r15037 | else: | ||
|
r15144 | row.append(l[pos:].strip()) | ||
|
r15037 | rows.append(row) | ||
|
r15144 | |||
|
r43347 | block[b'table'] = rows | ||
|
r15037 | |||
return blocks | ||||
|
r43346 | |||
|
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 | | ||||
# | ------------- | | ||||
# +------------------------------+ | ||||
|
r43346 | if ( | ||
|
r43347 | block[b'type'] == b'paragraph' | ||
and len(block[b'lines']) == 2 | ||||
and encoding.colwidth(block[b'lines'][0]) == len(block[b'lines'][1]) | ||||
and _sectionre.match(block[b'lines'][1]) | ||||
|
r43346 | ): | ||
|
r43347 | block[b'underline'] = block[b'lines'][1][0:1] | ||
block[b'type'] = b'section' | ||||
del block[b'lines'][1] | ||||
|
r9156 | return blocks | ||
|
r43346 | |||
|
r9623 | def inlineliterals(blocks): | ||
|
r43347 | substs = [(b'``', b'"')] | ||
|
r9623 | for b in blocks: | ||
|
r43347 | if b[b'type'] in (b'paragraph', b'section'): | ||
b[b'lines'] = [replace(l, substs) for l in b[b'lines']] | ||||
|
r9623 | return blocks | ||
|
r43346 | |||
|
r10972 | def hgrole(blocks): | ||
|
r43347 | substs = [(b':hg:`', b"'hg "), (b'`', b"'")] | ||
|
r10972 | for b in blocks: | ||
|
r43347 | if b[b'type'] in (b'paragraph', b'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). | ||||
|
r43347 | b[b'lines'] = [replace(l, substs) for l in b[b'lines']] | ||
|
r10972 | return blocks | ||
|
r43346 | |||
|
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): | ||||
|
r43347 | if blocks[i][b'type'] == blocks[i - 1][b'type'] and blocks[i][ | ||
b'type' | ||||
|
r46554 | ] in ( | ||
b'bullet', | ||||
b'option', | ||||
b'field', | ||||
): | ||||
|
r9156 | i += 1 | ||
|
r43347 | elif not blocks[i - 1][b'lines']: | ||
|
r20549 | # no lines in previous block, do not separate | ||
|
r19995 | i += 1 | ||
|
r9156 | else: | ||
|
r43347 | blocks.insert( | ||
i, {b'lines': [b''], b'indent': 0, b'type': b'margin'} | ||||
) | ||||
|
r9156 | i += 2 | ||
return blocks | ||||
|
r43346 | |||
|
r12819 | def prunecomments(blocks): | ||
"""Remove comments.""" | ||||
i = 0 | ||||
while i < len(blocks): | ||||
b = blocks[i] | ||||
|
r43347 | if b[b'type'] == b'paragraph' and ( | ||
b[b'lines'][0].startswith(b'.. ') or b[b'lines'] == [b'..'] | ||||
|
r43346 | ): | ||
|
r12819 | del blocks[i] | ||
|
r43347 | if i < len(blocks) and blocks[i][b'type'] == b'margin': | ||
|
r13003 | del blocks[i] | ||
|
r12819 | else: | ||
i += 1 | ||||
return blocks | ||||
|
r31131 | |||
|
r31132 | def findadmonitions(blocks, admonitions=None): | ||
|
r12388 | """ | ||
Makes the type of the block an admonition block if | ||||
the first line is an admonition directive | ||||
""" | ||||
|
r31714 | admonitions = admonitions or _admonitiontitles.keys() | ||
|
r31132 | |||
|
r43346 | admonitionre = re.compile( | ||
|
r43347 | br'\.\. (%s)::' % b'|'.join(sorted(admonitions)), flags=re.IGNORECASE | ||
|
r43346 | ) | ||
|
r31131 | |||
|
r12388 | i = 0 | ||
while i < len(blocks): | ||||
|
r43347 | m = admonitionre.match(blocks[i][b'lines'][0]) | ||
|
r12388 | if m: | ||
|
r43347 | blocks[i][b'type'] = b'admonition' | ||
admonitiontitle = blocks[i][b'lines'][0][3 : m.end() - 2].lower() | ||||
|
r12388 | |||
|
r43347 | firstline = blocks[i][b'lines'][0][m.end() + 1 :] | ||
|
r12620 | if firstline: | ||
|
r43347 | blocks[i][b'lines'].insert(1, b' ' + firstline) | ||
|
r12388 | |||
|
r43347 | blocks[i][b'admonitiontitle'] = admonitiontitle | ||
del blocks[i][b'lines'][0] | ||||
|
r12388 | i = i + 1 | ||
return blocks | ||||
|
r9156 | |||
|
r43346 | |||
|
r31712 | _admonitiontitles = { | ||
|
r43347 | b'attention': _(b'Attention:'), | ||
b'caution': _(b'Caution:'), | ||||
b'danger': _(b'!Danger!'), | ||||
b'error': _(b'Error:'), | ||||
b'hint': _(b'Hint:'), | ||||
b'important': _(b'Important:'), | ||||
b'note': _(b'Note:'), | ||||
b'tip': _(b'Tip:'), | ||||
b'warning': _(b'Warning!'), | ||||
|
r31712 | } | ||
|
r12652 | |||
|
r43346 | |||
|
r13011 | def formatoption(block, width): | ||
|
r43347 | desc = b' '.join(map(bytes.strip, block[b'lines'])) | ||
colwidth = encoding.colwidth(block[b'optstr']) | ||||
|
r13011 | usablewidth = width - 1 | ||
|
r43347 | hanging = block[b'optstrwidth'] | ||
initindent = b'%s%s ' % (block[b'optstr'], b' ' * ((hanging - colwidth))) | ||||
hangindent = b' ' * (encoding.colwidth(initindent) + 1) | ||||
return b' %s\n' % ( | ||||
|
r43346 | stringutil.wrap( | ||
desc, usablewidth, initindent=initindent, hangindent=hangindent | ||||
) | ||||
) | ||||
|
r13011 | |||
|
r9156 | def formatblock(block, width): | ||
"""Format a block according to width.""" | ||||
|
r9417 | if width <= 0: | ||
width = 78 | ||||
|
r43347 | indent = b' ' * block[b'indent'] | ||
if block[b'type'] == b'admonition': | ||||
admonition = _admonitiontitles[block[b'admonitiontitle']] | ||||
if not block[b'lines']: | ||||
return indent + admonition + b'\n' | ||||
hang = len(block[b'lines'][-1]) - len(block[b'lines'][-1].lstrip()) | ||||
|
r12388 | |||
|
r43347 | defindent = indent + hang * b' ' | ||
text = b' '.join(map(bytes.strip, block[b'lines'])) | ||||
return b'%s\n%s\n' % ( | ||||
|
r43346 | indent + admonition, | ||
stringutil.wrap( | ||||
text, width=width, initindent=defindent, hangindent=defindent | ||||
), | ||||
) | ||||
|
r43347 | if block[b'type'] == b'margin': | ||
return b'\n' | ||||
if block[b'type'] == b'literal': | ||||
indent += b' ' | ||||
return indent + (b'\n' + indent).join(block[b'lines']) + b'\n' | ||||
if block[b'type'] == b'section': | ||||
underline = encoding.colwidth(block[b'lines'][0]) * block[b'underline'] | ||||
return b"%s%s\n%s%s\n" % (indent, block[b'lines'][0], indent, underline) | ||||
if block[b'type'] == b'table': | ||||
table = block[b'table'] | ||||
|
r15037 | # compute column widths | ||
widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)] | ||||
|
r43347 | text = b'' | ||
|
r15037 | span = sum(widths) + len(widths) - 1 | ||
|
r43347 | indent = b' ' * block[b'indent'] | ||
hang = b' ' * (len(indent) + span - widths[-1]) | ||||
|
r15037 | |||
for row in table: | ||||
|
r15144 | l = [] | ||
for w, v in zip(widths, row): | ||||
|
r43347 | pad = b' ' * (w - encoding.colwidth(v)) | ||
|
r15144 | l.append(v + pad) | ||
|
r43347 | l = b' '.join(l) | ||
|
r43346 | l = stringutil.wrap( | ||
l, width=width, initindent=indent, hangindent=hang | ||||
) | ||||
|
r43347 | if not text and block[b'header']: | ||
text = l + b'\n' + indent + b'-' * (min(width, span)) + b'\n' | ||||
|
r15037 | else: | ||
|
r43347 | text += l + b"\n" | ||
|
r15037 | return text | ||
|
r43347 | if block[b'type'] == b'definition': | ||
term = indent + block[b'lines'][0] | ||||
hang = len(block[b'lines'][-1]) - len(block[b'lines'][-1].lstrip()) | ||||
defindent = indent + hang * b' ' | ||||
text = b' '.join(map(bytes.strip, block[b'lines'][1:])) | ||||
return b'%s\n%s\n' % ( | ||||
|
r43346 | term, | ||
stringutil.wrap( | ||||
text, width=width, initindent=defindent, hangindent=defindent | ||||
), | ||||
) | ||||
|
r10937 | subindent = indent | ||
|
r43347 | if block[b'type'] == b'bullet': | ||
if block[b'lines'][0].startswith(b'| '): | ||||
|
r10447 | # Remove bullet for line blocks and add no extra | ||
|
r26781 | # indentation. | ||
|
r43347 | block[b'lines'][0] = block[b'lines'][0][2:] | ||
|
r10447 | else: | ||
|
r43347 | m = _bulletre.match(block[b'lines'][0]) | ||
subindent = indent + m.end() * b' ' | ||||
elif block[b'type'] == b'field': | ||||
key = block[b'key'] | ||||
subindent = indent + _fieldwidth * b' ' | ||||
|
r10065 | 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) | ||
|
r43347 | block[b'lines'][0] = key + block[b'lines'][0] | ||
elif block[b'type'] == b'option': | ||||
|
r13011 | return formatoption(block, width) | ||
|
r9156 | |||
|
r43347 | text = b' '.join(map(bytes.strip, block[b'lines'])) | ||
|
r43346 | return ( | ||
stringutil.wrap( | ||||
text, width=width, initindent=indent, hangindent=subindent | ||||
) | ||||
|
r43347 | + b'\n' | ||
|
r43346 | ) | ||
|
r9156 | |||
|
r15261 | def formathtml(blocks): | ||
"""Format RST blocks as HTML""" | ||||
out = [] | ||||
|
r43347 | headernest = b'' | ||
|
r15261 | listnest = [] | ||
|
r18750 | def escape(s): | ||
|
r34696 | return url.escape(s, True) | ||
|
r18750 | |||
|
r15261 | def openlist(start, level): | ||
if not listnest or listnest[-1][0] != start: | ||||
listnest.append((start, level)) | ||||
|
r43347 | out.append(b'<%s>\n' % start) | ||
|
r15261 | |||
|
r43347 | blocks = [b for b in blocks if b[b'type'] != b'margin'] | ||
|
r15261 | |||
for pos, b in enumerate(blocks): | ||||
|
r43347 | btype = b[b'type'] | ||
level = b[b'indent'] | ||||
lines = b[b'lines'] | ||||
|
r15261 | |||
|
r43347 | if btype == b'admonition': | ||
admonition = escape(_admonitiontitles[b[b'admonitiontitle']]) | ||||
text = escape(b' '.join(map(bytes.strip, lines))) | ||||
out.append(b'<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text)) | ||||
elif btype == b'paragraph': | ||||
out.append(b'<p>\n%s\n</p>\n' % escape(b'\n'.join(lines))) | ||||
elif btype == b'margin': | ||||
|
r15261 | pass | ||
|
r43347 | elif btype == b'literal': | ||
out.append(b'<pre>\n%s\n</pre>\n' % escape(b'\n'.join(lines))) | ||||
elif btype == b'section': | ||||
i = b[b'underline'] | ||||
|
r15261 | if i not in headernest: | ||
headernest += i | ||||
level = headernest.index(i) + 1 | ||||
|
r43347 | out.append(b'<h%d>%s</h%d>\n' % (level, escape(lines[0]), level)) | ||
elif btype == b'table': | ||||
table = b[b'table'] | ||||
out.append(b'<table>\n') | ||||
|
r15261 | for row in table: | ||
|
r43347 | out.append(b'<tr>') | ||
|
r18750 | for v in row: | ||
|
r43347 | out.append(b'<td>') | ||
|
r18752 | out.append(escape(v)) | ||
|
r43347 | out.append(b'</td>') | ||
out.append(b'\n') | ||||
|
r18752 | out.pop() | ||
|
r43347 | out.append(b'</tr>\n') | ||
out.append(b'</table>\n') | ||||
elif btype == b'definition': | ||||
openlist(b'dl', level) | ||||
|
r18750 | term = escape(lines[0]) | ||
|
r43347 | text = escape(b' '.join(map(bytes.strip, lines[1:]))) | ||
out.append(b' <dt>%s\n <dd>%s\n' % (term, text)) | ||||
elif btype == b'bullet': | ||||
bullet, head = lines[0].split(b' ', 1) | ||||
if bullet in (b'*', b'-'): | ||||
openlist(b'ul', level) | ||||
|
r15261 | else: | ||
|
r43347 | openlist(b'ol', level) | ||
out.append(b' <li> %s\n' % escape(b' '.join([head] + lines[1:]))) | ||||
elif btype == b'field': | ||||
openlist(b'dl', level) | ||||
key = escape(b[b'key']) | ||||
text = escape(b' '.join(map(bytes.strip, lines))) | ||||
out.append(b' <dt>%s\n <dd>%s\n' % (key, text)) | ||||
elif btype == b'option': | ||||
openlist(b'dl', level) | ||||
opt = escape(b[b'optstr']) | ||||
desc = escape(b' '.join(map(bytes.strip, lines))) | ||||
out.append(b' <dt>%s\n <dd>%s\n' % (opt, desc)) | ||||
|
r15261 | |||
# close lists if indent level of next block is lower | ||||
if listnest: | ||||
start, level = listnest[-1] | ||||
if pos == len(blocks) - 1: | ||||
|
r43347 | out.append(b'</%s>\n' % start) | ||
|
r15261 | listnest.pop() | ||
else: | ||||
nb = blocks[pos + 1] | ||||
|
r43347 | ni = nb[b'indent'] | ||
|
r43346 | if ni < level or ( | ||
ni == level | ||||
|
r43347 | and nb[b'type'] not in b'definition bullet field option' | ||
|
r43346 | ): | ||
|
r43347 | out.append(b'</%s>\n' % start) | ||
|
r15261 | listnest.pop() | ||
|
r43347 | return b''.join(out) | ||
|
r15261 | |||
|
r43346 | |||
|
r31132 | def parse(text, indent=0, keep=None, admonitions=None): | ||
|
r15012 | """Parse text into a list of blocks""" | ||
|
r9156 | blocks = findblocks(text) | ||
|
r9540 | for b in blocks: | ||
|
r43347 | b[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) | ||
|
r31132 | blocks = findadmonitions(blocks, admonitions=admonitions) | ||
|
r13003 | blocks = addmargins(blocks) | ||
|
r12819 | blocks = prunecomments(blocks) | ||
|
r15012 | return blocks, pruned | ||
|
r43346 | |||
|
r15013 | def formatblocks(blocks, width): | ||
|
r43347 | text = b''.join(formatblock(b, width) for b in blocks) | ||
|
r15013 | return text | ||
|
r43346 | |||
|
r39343 | def formatplain(blocks, width): | ||
"""Format parsed blocks as plain text""" | ||||
|
r43347 | return b''.join(formatblock(b, width) for b in blocks) | ||
|
r39343 | |||
|
r43346 | |||
|
r43347 | def format(text, width=80, indent=0, keep=None, style=b'plain', section=None): | ||
|
r15012 | """Parse and format the text according to width.""" | ||
blocks, pruned = parse(text, indent, keep or []) | ||||
|
r39341 | if section: | ||
blocks = filtersections(blocks, section) | ||||
|
r43347 | if style == b'html': | ||
|
r39346 | return formathtml(blocks) | ||
|
r39341 | else: | ||
|
r39346 | return formatplain(blocks, width=width) | ||
|
r39341 | |||
|
r43346 | |||
|
r39341 | def filtersections(blocks, section): | ||
|
r39377 | """Select parsed blocks under the specified section | ||
The section name is separated by a dot, and matches the suffix of the | ||||
full section path. | ||||
""" | ||||
|
r26113 | parents = [] | ||
|
r39376 | sections = _getsections(blocks) | ||
|
r39342 | blocks = [] | ||
i = 0 | ||||
lastparents = [] | ||||
synthetic = [] | ||||
collapse = True | ||||
while i < len(sections): | ||||
|
r39377 | path, nest, b = sections[i] | ||
|
r39342 | del parents[nest:] | ||
parents.append(i) | ||||
|
r43347 | if path == section or path.endswith(b'.' + section): | ||
|
r39342 | if lastparents != parents: | ||
llen = len(lastparents) | ||||
plen = len(parents) | ||||
if llen and llen != plen: | ||||
collapse = False | ||||
s = [] | ||||
for j in pycompat.xrange(3, plen - 1): | ||||
parent = parents[j] | ||||
|
r43346 | if j >= llen or lastparents[j] != parent: | ||
|
r39342 | s.append(len(blocks)) | ||
sec = sections[parent][2] | ||||
blocks.append(sec[0]) | ||||
blocks.append(sec[-1]) | ||||
if s: | ||||
synthetic.append(s) | ||||
|
r27614 | |||
|
r39342 | lastparents = parents[:] | ||
blocks.extend(b) | ||||
|
r22770 | |||
|
r39342 | ## Also show all subnested sections | ||
while i + 1 < len(sections) and sections[i + 1][1] > nest: | ||||
i += 1 | ||||
blocks.extend(sections[i][2]) | ||||
i += 1 | ||||
if collapse: | ||||
synthetic.reverse() | ||||
for s in synthetic: | ||||
|
r43347 | path = [blocks[syn][b'lines'][0] for syn in s] | ||
|
r39342 | real = s[-1] + 2 | ||
|
r43347 | realline = blocks[real][b'lines'] | ||
realline[0] = b'"%s"' % b'.'.join(path + [realline[0]]).replace( | ||||
b'"', b'' | ||||
|
r43346 | ) | ||
del blocks[s[0] : real] | ||||
|
r22770 | |||
|
r39341 | return blocks | ||
|
r9156 | |||
|
r43346 | |||
|
r39376 | def _getsections(blocks): | ||
|
r39377 | '''return a list of (section path, nesting level, blocks) tuples''' | ||
|
r43347 | nest = b"" | ||
|
r39377 | names = () | ||
|
r15014 | secs = [] | ||
|
r22587 | |||
def getname(b): | ||||
|
r43347 | if b[b'type'] == b'field': | ||
x = b[b'key'] | ||||
|
r25723 | else: | ||
|
r43347 | x = b[b'lines'][0] | ||
x = encoding.lower(x).strip(b'"') | ||||
if b'(' in x: | ||||
x = x.split(b'(')[0] | ||||
|
r22587 | return x | ||
|
r15014 | for b in blocks: | ||
|
r43347 | if b[b'type'] == b'section': | ||
i = b[b'underline'] | ||||
|
r15014 | if i not in nest: | ||
nest += i | ||||
level = nest.index(i) + 1 | ||||
nest = nest[:level] | ||||
|
r39377 | names = names[:level] + (getname(b),) | ||
|
r43347 | secs.append((b'.'.join(names), level, [b])) | ||
elif b[b'type'] in (b'definition', b'field'): | ||||
i = b' ' | ||||
|
r22587 | if i not in nest: | ||
nest += i | ||||
level = nest.index(i) + 1 | ||||
nest = nest[:level] | ||||
|
r26237 | for i in range(1, len(secs) + 1): | ||
sec = secs[-i] | ||||
if sec[1] < level: | ||||
break | ||||
|
r43347 | siblings = [a for a in sec[2] if a[b'type'] == b'definition'] | ||
|
r26237 | if siblings: | ||
|
r43347 | siblingindent = siblings[-1][b'indent'] | ||
indent = b[b'indent'] | ||||
|
r26237 | if siblingindent < indent: | ||
level += 1 | ||||
break | ||||
elif siblingindent == indent: | ||||
level = sec[1] | ||||
break | ||||
|
r39377 | names = names[:level] + (getname(b),) | ||
|
r43347 | secs.append((b'.'.join(names), level, [b])) | ||
|
r15014 | else: | ||
if not secs: | ||||
# add an initial empty section | ||||
|
r43347 | secs = [(b'', 0, [])] | ||
if b[b'type'] != b'margin': | ||||
|
r26157 | pointer = 1 | ||
|
r43347 | bindent = b[b'indent'] | ||
|
r26157 | while pointer < len(secs): | ||
section = secs[-pointer][2][0] | ||||
|
r43347 | if section[b'type'] != b'margin': | ||
sindent = section[b'indent'] | ||||
if len(section[b'lines']) > 1: | ||||
sindent += len(section[b'lines'][1]) - len( | ||||
section[b'lines'][1].lstrip(b' ') | ||||
|
r43346 | ) | ||
|
r26157 | if bindent >= sindent: | ||
break | ||||
pointer += 1 | ||||
if pointer > 1: | ||||
|
r26170 | blevel = secs[-pointer][1] | ||
|
r43347 | if section[b'type'] != b[b'type']: | ||
|
r26170 | blevel += 1 | ||
|
r43347 | secs.append((b'', blevel, [])) | ||
|
r15014 | secs[-1][2].append(b) | ||
return secs | ||||
|
r9156 | |||
|
r43346 | |||
|
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)] | ||||
|
r43347 | indent = b' ' * indent | ||
div = indent + b' '.join(b'=' * w for w in widths) + b'\n' | ||||
|
r15039 | |||
out = [div] | ||||
for row in data: | ||||
|
r15144 | l = [] | ||
for w, v in zip(widths, row): | ||||
|
r43347 | if b'\n' in v: | ||
|
r20654 | # only remove line breaks and indentation, long lines are | ||
# handled by the next tool | ||||
|
r43347 | v = b' '.join(e.lstrip() for e in v.split(b'\n')) | ||
pad = b' ' * (w - encoding.colwidth(v)) | ||||
|
r15144 | l.append(v + pad) | ||
|
r43347 | out.append(indent + b' '.join(l) + b"\n") | ||
|
r15039 | if header and len(data) > 1: | ||
out.insert(2, div) | ||||
out.append(div) | ||||
|
r16815 | return out | ||