883 lines
30 KiB
Python
883 lines
30 KiB
Python
|
from __future__ import absolute_import, unicode_literals, division
|
||
|
|
||
|
import re
|
||
|
import sys
|
||
|
from commonmark import common
|
||
|
from commonmark.common import normalize_uri, unescape_string
|
||
|
from commonmark.node import Node
|
||
|
from commonmark.normalize_reference import normalize_reference
|
||
|
|
||
|
if sys.version_info >= (3, 0):
|
||
|
if sys.version_info >= (3, 4):
|
||
|
import html
|
||
|
HTMLunescape = html.unescape
|
||
|
else:
|
||
|
from .entitytrans import _unescape
|
||
|
HTMLunescape = _unescape
|
||
|
else:
|
||
|
from commonmark import entitytrans
|
||
|
HTMLunescape = entitytrans._unescape
|
||
|
|
||
|
# Some regexps used in inline parser:
|
||
|
|
||
|
ESCAPED_CHAR = '\\\\' + common.ESCAPABLE
|
||
|
|
||
|
rePunctuation = re.compile(
|
||
|
r'[!"#$%&\'()*+,\-./:;<=>?@\[\]\\^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB'
|
||
|
r'\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3'
|
||
|
r'\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F'
|
||
|
r'\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E'
|
||
|
r'\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12'
|
||
|
r'\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB'
|
||
|
r'\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736'
|
||
|
r'\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-'
|
||
|
r'\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F'
|
||
|
r'\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E'
|
||
|
r'\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5'
|
||
|
r'\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC'
|
||
|
r'\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011'
|
||
|
r'\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673'
|
||
|
r'\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E'
|
||
|
r'\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0'
|
||
|
r'\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63'
|
||
|
r'\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B'
|
||
|
r'\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-'
|
||
|
r'\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58'
|
||
|
r'\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D'
|
||
|
r'\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD'
|
||
|
r'\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7'
|
||
|
r'\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F'
|
||
|
r'\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]'
|
||
|
)
|
||
|
|
||
|
reLinkTitle = re.compile(
|
||
|
'^(?:"(' + ESCAPED_CHAR + '|[^"\\x00])*"' +
|
||
|
'|' +
|
||
|
'\'(' + ESCAPED_CHAR + '|[^\'\\x00])*\'' +
|
||
|
'|' +
|
||
|
'\\((' + ESCAPED_CHAR + '|[^()\\x00])*\\))')
|
||
|
reLinkDestinationBraces = re.compile(r'^(?:<(?:[^<>\n\\\x00]|\\.)*>)')
|
||
|
|
||
|
reEscapable = re.compile('^' + common.ESCAPABLE)
|
||
|
reEntityHere = re.compile('^' + common.ENTITY, re.IGNORECASE)
|
||
|
reTicks = re.compile(r'`+')
|
||
|
reTicksHere = re.compile(r'^`+')
|
||
|
reEllipses = re.compile(r'\.\.\.')
|
||
|
reDash = re.compile(r'--+')
|
||
|
reEmailAutolink = re.compile(
|
||
|
r"^<([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9]"
|
||
|
r"(?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
|
||
|
r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)>")
|
||
|
reAutolink = re.compile(
|
||
|
r'^<[A-Za-z][A-Za-z0-9.+-]{1,31}:[^<>\x00-\x20]*>',
|
||
|
re.IGNORECASE)
|
||
|
reSpnl = re.compile(r'^ *(?:\n *)?')
|
||
|
reWhitespaceChar = re.compile(r'^^[ \t\n\x0b\x0c\x0d]')
|
||
|
reWhitespace = re.compile(r'[ \t\n\x0b\x0c\x0d]+')
|
||
|
reUnicodeWhitespaceChar = re.compile(r'^\s')
|
||
|
reFinalSpace = re.compile(r' *$')
|
||
|
reInitialSpace = re.compile(r'^ *')
|
||
|
reSpaceAtEndOfLine = re.compile(r'^ *(?:\n|$)')
|
||
|
reLinkLabel = re.compile(r'^\[(?:[^\\\[\]]|\\.){0,1000}\]')
|
||
|
# Matches a string of non-special characters.
|
||
|
reMain = re.compile(r'^[^\n`\[\]\\!<&*_\'"]+', re.MULTILINE)
|
||
|
|
||
|
|
||
|
def text(s):
|
||
|
node = Node('text', None)
|
||
|
node.literal = s
|
||
|
return node
|
||
|
|
||
|
|
||
|
def smart_dashes(chars):
|
||
|
en_count = 0
|
||
|
em_count = 0
|
||
|
if len(chars) % 3 == 0:
|
||
|
# If divisible by 3, use all em dashes
|
||
|
em_count = len(chars) // 3
|
||
|
elif len(chars) % 2 == 0:
|
||
|
# If divisble by 2, use all en dashes
|
||
|
en_count = len(chars) // 2
|
||
|
elif len(chars) % 3 == 2:
|
||
|
# if 2 extra dashes, use en dashfor last 2;
|
||
|
# em dashes for rest
|
||
|
en_count = 1
|
||
|
em_count = (len(chars) - 2) // 3
|
||
|
else:
|
||
|
# Use en dashes for last 4 hyphens; em dashes for rest
|
||
|
en_count = 2
|
||
|
em_count = (len(chars) - 4) // 3
|
||
|
return ('\u2014' * em_count) + ('\u2013' * en_count)
|
||
|
|
||
|
|
||
|
class InlineParser(object):
|
||
|
"""INLINE PARSER
|
||
|
|
||
|
These are methods of an InlineParser class, defined below.
|
||
|
An InlineParser keeps track of a subject (a string to be
|
||
|
parsed) and a position in that subject.
|
||
|
"""
|
||
|
|
||
|
def __init__(self, options={}):
|
||
|
self.subject = ''
|
||
|
self.brackets = None
|
||
|
self.pos = 0
|
||
|
self.refmap = {}
|
||
|
self.options = options
|
||
|
|
||
|
def match(self, regexString):
|
||
|
"""
|
||
|
If regexString matches at current position in the subject, advance
|
||
|
position in subject and return the match; otherwise return None.
|
||
|
"""
|
||
|
match = re.search(regexString, self.subject[self.pos:])
|
||
|
if match is None:
|
||
|
return None
|
||
|
else:
|
||
|
self.pos += match.end()
|
||
|
return match.group()
|
||
|
|
||
|
def peek(self):
|
||
|
""" Returns the character at the current subject position, or None if
|
||
|
there are no more characters."""
|
||
|
if self.pos < len(self.subject):
|
||
|
return self.subject[self.pos]
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
def spnl(self):
|
||
|
""" Parse zero or more space characters, including at
|
||
|
most one newline."""
|
||
|
self.match(reSpnl)
|
||
|
return True
|
||
|
|
||
|
# All of the parsers below try to match something at the current position
|
||
|
# in the subject. If they succeed in matching anything, they
|
||
|
# push an inline matched, advancing the subject.
|
||
|
|
||
|
def parseBackticks(self, block):
|
||
|
""" Attempt to parse backticks, adding either a backtick code span or a
|
||
|
literal sequence of backticks to the 'inlines' list."""
|
||
|
ticks = self.match(reTicksHere)
|
||
|
if ticks is None:
|
||
|
return False
|
||
|
after_open_ticks = self.pos
|
||
|
matched = self.match(reTicks)
|
||
|
while matched is not None:
|
||
|
if matched == ticks:
|
||
|
node = Node('code', None)
|
||
|
contents = self.subject[after_open_ticks:self.pos-len(ticks)] \
|
||
|
.replace('\n', ' ')
|
||
|
if contents.lstrip(' ') and contents[0] == contents[-1] == ' ':
|
||
|
node.literal = contents[1:-1]
|
||
|
else:
|
||
|
node.literal = contents
|
||
|
block.append_child(node)
|
||
|
return True
|
||
|
matched = self.match(reTicks)
|
||
|
# If we got here, we didn't match a closing backtick sequence.
|
||
|
self.pos = after_open_ticks
|
||
|
block.append_child(text(ticks))
|
||
|
return True
|
||
|
|
||
|
def parseBackslash(self, block):
|
||
|
"""
|
||
|
Parse a backslash-escaped special character, adding either the
|
||
|
escaped character, a hard line break (if the backslash is followed
|
||
|
by a newline), or a literal backslash to the block's children.
|
||
|
Assumes current character is a backslash.
|
||
|
"""
|
||
|
subj = self.subject
|
||
|
self.pos += 1
|
||
|
|
||
|
try:
|
||
|
subjchar = subj[self.pos]
|
||
|
except IndexError:
|
||
|
subjchar = None
|
||
|
|
||
|
if self.peek() == '\n':
|
||
|
self.pos += 1
|
||
|
node = Node('linebreak', None)
|
||
|
block.append_child(node)
|
||
|
elif subjchar and re.search(reEscapable, subjchar):
|
||
|
block.append_child(text(subjchar))
|
||
|
self.pos += 1
|
||
|
else:
|
||
|
block.append_child(text('\\'))
|
||
|
|
||
|
return True
|
||
|
|
||
|
def parseAutolink(self, block):
|
||
|
"""Attempt to parse an autolink (URL or email in pointy brackets)."""
|
||
|
m = self.match(reEmailAutolink)
|
||
|
|
||
|
if m:
|
||
|
# email
|
||
|
dest = m[1:-1]
|
||
|
node = Node('link', None)
|
||
|
node.destination = normalize_uri('mailto:' + dest)
|
||
|
node.title = ''
|
||
|
node.append_child(text(dest))
|
||
|
block.append_child(node)
|
||
|
return True
|
||
|
else:
|
||
|
m = self.match(reAutolink)
|
||
|
if m:
|
||
|
# link
|
||
|
dest = m[1:-1]
|
||
|
node = Node('link', None)
|
||
|
node.destination = normalize_uri(dest)
|
||
|
node.title = ''
|
||
|
node.append_child(text(dest))
|
||
|
block.append_child(node)
|
||
|
return True
|
||
|
|
||
|
return False
|
||
|
|
||
|
def parseHtmlTag(self, block):
|
||
|
"""Attempt to parse a raw HTML tag."""
|
||
|
m = self.match(common.reHtmlTag)
|
||
|
if m is None:
|
||
|
return False
|
||
|
else:
|
||
|
node = Node('html_inline', None)
|
||
|
node.literal = m
|
||
|
block.append_child(node)
|
||
|
return True
|
||
|
|
||
|
def scanDelims(self, c):
|
||
|
"""
|
||
|
Scan a sequence of characters == c, and return information about
|
||
|
the number of delimiters and whether they are positioned such that
|
||
|
they can open and/or close emphasis or strong emphasis. A utility
|
||
|
function for strong/emph parsing.
|
||
|
"""
|
||
|
numdelims = 0
|
||
|
startpos = self.pos
|
||
|
|
||
|
if c == "'" or c == '"':
|
||
|
numdelims += 1
|
||
|
self.pos += 1
|
||
|
else:
|
||
|
while (self.peek() == c):
|
||
|
numdelims += 1
|
||
|
self.pos += 1
|
||
|
|
||
|
if numdelims == 0:
|
||
|
return None
|
||
|
|
||
|
c_before = '\n' if startpos == 0 else self.subject[startpos - 1]
|
||
|
|
||
|
c_after = self.peek()
|
||
|
if c_after is None:
|
||
|
c_after = '\n'
|
||
|
|
||
|
# Python 2 doesn't recognize '\xa0' as whitespace
|
||
|
after_is_whitespace = re.search(reUnicodeWhitespaceChar, c_after) or \
|
||
|
c_after == '\xa0'
|
||
|
after_is_punctuation = re.search(rePunctuation, c_after)
|
||
|
before_is_whitespace = re.search(
|
||
|
reUnicodeWhitespaceChar, c_before) or \
|
||
|
c_before == '\xa0'
|
||
|
before_is_punctuation = re.search(rePunctuation, c_before)
|
||
|
|
||
|
left_flanking = not after_is_whitespace and \
|
||
|
(not after_is_punctuation or
|
||
|
before_is_whitespace or
|
||
|
before_is_punctuation)
|
||
|
right_flanking = not before_is_whitespace and \
|
||
|
(not before_is_punctuation or
|
||
|
after_is_whitespace or
|
||
|
after_is_punctuation)
|
||
|
if c == '_':
|
||
|
can_open = left_flanking and \
|
||
|
(not right_flanking or before_is_punctuation)
|
||
|
can_close = right_flanking and \
|
||
|
(not left_flanking or after_is_punctuation)
|
||
|
elif c == "'" or c == '"':
|
||
|
can_open = left_flanking and not right_flanking
|
||
|
can_close = right_flanking
|
||
|
else:
|
||
|
can_open = left_flanking
|
||
|
can_close = right_flanking
|
||
|
|
||
|
self.pos = startpos
|
||
|
return {
|
||
|
'numdelims': numdelims,
|
||
|
'can_open': can_open,
|
||
|
'can_close': can_close,
|
||
|
}
|
||
|
|
||
|
def handleDelim(self, cc, block):
|
||
|
"""Handle a delimiter marker for emphasis or a quote."""
|
||
|
res = self.scanDelims(cc)
|
||
|
if not res:
|
||
|
return False
|
||
|
numdelims = res.get('numdelims')
|
||
|
startpos = self.pos
|
||
|
|
||
|
self.pos += numdelims
|
||
|
if cc == "'":
|
||
|
contents = '\u2019'
|
||
|
elif cc == '"':
|
||
|
contents = '\u201C'
|
||
|
else:
|
||
|
contents = self.subject[startpos:self.pos]
|
||
|
node = text(contents)
|
||
|
block.append_child(node)
|
||
|
|
||
|
# Add entry to stack for this opener
|
||
|
self.delimiters = {
|
||
|
'cc': cc,
|
||
|
'numdelims': numdelims,
|
||
|
'origdelims': numdelims,
|
||
|
'node': node,
|
||
|
'previous': self.delimiters,
|
||
|
'next': None,
|
||
|
'can_open': res.get('can_open'),
|
||
|
'can_close': res.get('can_close'),
|
||
|
}
|
||
|
if self.delimiters['previous'] is not None:
|
||
|
self.delimiters['previous']['next'] = self.delimiters
|
||
|
return True
|
||
|
|
||
|
def removeDelimiter(self, delim):
|
||
|
if delim.get('previous') is not None:
|
||
|
delim['previous']['next'] = delim.get('next')
|
||
|
if delim.get('next') is None:
|
||
|
# Top of stack
|
||
|
self.delimiters = delim.get('previous')
|
||
|
else:
|
||
|
delim['next']['previous'] = delim.get('previous')
|
||
|
|
||
|
@staticmethod
|
||
|
def removeDelimitersBetween(bottom, top):
|
||
|
if bottom.get('next') != top:
|
||
|
bottom['next'] = top
|
||
|
top['previous'] = bottom
|
||
|
|
||
|
def processEmphasis(self, stack_bottom):
|
||
|
openers_bottom = {
|
||
|
'_': stack_bottom,
|
||
|
'*': stack_bottom,
|
||
|
"'": stack_bottom,
|
||
|
'"': stack_bottom,
|
||
|
}
|
||
|
odd_match = False
|
||
|
use_delims = 0
|
||
|
|
||
|
# Find first closer above stack_bottom
|
||
|
closer = self.delimiters
|
||
|
while closer is not None and closer.get('previous') != stack_bottom:
|
||
|
closer = closer.get('previous')
|
||
|
|
||
|
# Move forward, looking for closers, and handling each
|
||
|
while closer is not None:
|
||
|
if not closer.get('can_close'):
|
||
|
closer = closer.get('next')
|
||
|
else:
|
||
|
# found emphasis closer. now look back for first
|
||
|
# matching opener:
|
||
|
opener = closer.get('previous')
|
||
|
opener_found = False
|
||
|
closercc = closer.get('cc')
|
||
|
while (opener is not None and opener != stack_bottom and
|
||
|
opener != openers_bottom[closercc]):
|
||
|
odd_match = (closer.get('can_open') or
|
||
|
opener.get('can_close')) and \
|
||
|
closer['origdelims'] % 3 != 0 and \
|
||
|
(opener['origdelims'] +
|
||
|
closer['origdelims']) % 3 == 0
|
||
|
if opener.get('cc') == closercc and \
|
||
|
opener.get('can_open') and \
|
||
|
not odd_match:
|
||
|
opener_found = True
|
||
|
break
|
||
|
opener = opener.get('previous')
|
||
|
old_closer = closer
|
||
|
|
||
|
if closercc == '*' or closercc == '_':
|
||
|
if not opener_found:
|
||
|
closer = closer.get('next')
|
||
|
else:
|
||
|
# Calculate actual number of delimiters used from
|
||
|
# closer
|
||
|
use_delims = 2 if (
|
||
|
closer['numdelims'] >= 2 and
|
||
|
opener['numdelims'] >= 2) else 1
|
||
|
|
||
|
opener_inl = opener.get('node')
|
||
|
closer_inl = closer.get('node')
|
||
|
|
||
|
# Remove used delimiters from stack elts and inlines
|
||
|
opener['numdelims'] -= use_delims
|
||
|
closer['numdelims'] -= use_delims
|
||
|
opener_inl.literal = opener_inl.literal[
|
||
|
:len(opener_inl.literal) - use_delims]
|
||
|
closer_inl.literal = closer_inl.literal[
|
||
|
:len(closer_inl.literal) - use_delims]
|
||
|
|
||
|
# Build contents for new Emph element
|
||
|
if use_delims == 1:
|
||
|
emph = Node('emph', None)
|
||
|
else:
|
||
|
emph = Node('strong', None)
|
||
|
|
||
|
tmp = opener_inl.nxt
|
||
|
while tmp and tmp != closer_inl:
|
||
|
nxt = tmp.nxt
|
||
|
tmp.unlink()
|
||
|
emph.append_child(tmp)
|
||
|
tmp = nxt
|
||
|
|
||
|
opener_inl.insert_after(emph)
|
||
|
|
||
|
# Remove elts between opener and closer in delimiters
|
||
|
# stack
|
||
|
self.removeDelimitersBetween(opener, closer)
|
||
|
|
||
|
# If opener has 0 delims, remove it and the inline
|
||
|
if opener['numdelims'] == 0:
|
||
|
opener_inl.unlink()
|
||
|
self.removeDelimiter(opener)
|
||
|
|
||
|
if closer['numdelims'] == 0:
|
||
|
closer_inl.unlink()
|
||
|
tempstack = closer['next']
|
||
|
self.removeDelimiter(closer)
|
||
|
closer = tempstack
|
||
|
|
||
|
elif closercc == "'":
|
||
|
closer['node'].literal = '\u2019'
|
||
|
if opener_found:
|
||
|
opener['node'].literal = '\u2018'
|
||
|
closer = closer['next']
|
||
|
|
||
|
elif closercc == '"':
|
||
|
closer['node'].literal = '\u201D'
|
||
|
if opener_found:
|
||
|
opener['node'].literal = '\u201C'
|
||
|
closer = closer['next']
|
||
|
|
||
|
if not opener_found and not odd_match:
|
||
|
# Set lower bound for future searches for openers:
|
||
|
# We don't do this with odd_match because a **
|
||
|
# that doesn't match an earlier * might turn into
|
||
|
# an opener, and the * might be matched by something
|
||
|
# else.
|
||
|
openers_bottom[closercc] = old_closer['previous']
|
||
|
if not old_closer['can_open']:
|
||
|
# We can remove a closer that can't be an opener,
|
||
|
# once we've seen there's no matching opener:
|
||
|
self.removeDelimiter(old_closer)
|
||
|
|
||
|
# Remove all delimiters
|
||
|
while self.delimiters is not None and self.delimiters != stack_bottom:
|
||
|
self.removeDelimiter(self.delimiters)
|
||
|
|
||
|
def parseLinkTitle(self):
|
||
|
"""
|
||
|
Attempt to parse link title (sans quotes), returning the string
|
||
|
or None if no match.
|
||
|
"""
|
||
|
title = self.match(reLinkTitle)
|
||
|
if title is None:
|
||
|
return None
|
||
|
else:
|
||
|
# chop off quotes from title and unescape:
|
||
|
return unescape_string(title[1:-1])
|
||
|
|
||
|
def parseLinkDestination(self):
|
||
|
"""
|
||
|
Attempt to parse link destination, returning the string or
|
||
|
None if no match.
|
||
|
"""
|
||
|
res = self.match(reLinkDestinationBraces)
|
||
|
if res is None:
|
||
|
if self.peek() == '<':
|
||
|
return None
|
||
|
# TODO handrolled parser; res should be None or the string
|
||
|
savepos = self.pos
|
||
|
openparens = 0
|
||
|
while True:
|
||
|
c = self.peek()
|
||
|
if c is None:
|
||
|
break
|
||
|
if c == '\\' and re.search(
|
||
|
reEscapable, self.subject[self.pos+1:self.pos+2]):
|
||
|
self.pos += 1
|
||
|
if self.peek() is not None:
|
||
|
self.pos += 1
|
||
|
elif c == '(':
|
||
|
self.pos += 1
|
||
|
openparens += 1
|
||
|
elif c == ')':
|
||
|
if openparens < 1:
|
||
|
break
|
||
|
else:
|
||
|
self.pos += 1
|
||
|
openparens -= 1
|
||
|
elif re.search(reWhitespaceChar, c):
|
||
|
break
|
||
|
else:
|
||
|
self.pos += 1
|
||
|
if self.pos == savepos and c != ')':
|
||
|
return None
|
||
|
res = self.subject[savepos:self.pos]
|
||
|
return normalize_uri(unescape_string(res))
|
||
|
else:
|
||
|
# chop off surrounding <..>:
|
||
|
return normalize_uri(unescape_string(res[1:-1]))
|
||
|
|
||
|
def parseLinkLabel(self):
|
||
|
"""
|
||
|
Attempt to parse a link label, returning number of
|
||
|
characters parsed.
|
||
|
"""
|
||
|
# Note: our regex will allow something of form [..\];
|
||
|
# we disallow it here rather than using lookahead in the regex:
|
||
|
m = self.match(reLinkLabel)
|
||
|
if m is None or len(m) > 1001:
|
||
|
return 0
|
||
|
else:
|
||
|
return len(m)
|
||
|
|
||
|
def parseOpenBracket(self, block):
|
||
|
"""
|
||
|
Add open bracket to delimiter stack and add a text node to
|
||
|
block's children.
|
||
|
"""
|
||
|
startpos = self.pos
|
||
|
self.pos += 1
|
||
|
|
||
|
node = text('[')
|
||
|
block.append_child(node)
|
||
|
|
||
|
# Add entry to stack for this opener
|
||
|
self.addBracket(node, startpos, False)
|
||
|
return True
|
||
|
|
||
|
def parseBang(self, block):
|
||
|
"""
|
||
|
If next character is [, and ! delimiter to delimiter stack and
|
||
|
add a text node to block's children. Otherwise just add a text
|
||
|
node.
|
||
|
"""
|
||
|
startpos = self.pos
|
||
|
self.pos += 1
|
||
|
if self.peek() == '[':
|
||
|
self.pos += 1
|
||
|
|
||
|
node = text('![')
|
||
|
block.append_child(node)
|
||
|
|
||
|
# Add entry to stack for this openeer
|
||
|
self.addBracket(node, startpos + 1, True)
|
||
|
else:
|
||
|
block.append_child(text('!'))
|
||
|
|
||
|
return True
|
||
|
|
||
|
def parseCloseBracket(self, block):
|
||
|
"""
|
||
|
Try to match close bracket against an opening in the delimiter
|
||
|
stack. Add either a link or image, or a plain [ character,
|
||
|
to block's children. If there is a matching delimiter,
|
||
|
remove it from the delimiter stack.
|
||
|
"""
|
||
|
title = None
|
||
|
matched = False
|
||
|
self.pos += 1
|
||
|
startpos = self.pos
|
||
|
|
||
|
# get last [ or ![
|
||
|
opener = self.brackets
|
||
|
|
||
|
if opener is None:
|
||
|
# no matched opener, just return a literal
|
||
|
block.append_child(text(']'))
|
||
|
return True
|
||
|
|
||
|
if not opener.get('active'):
|
||
|
# no matched opener, just return a literal
|
||
|
block.append_child(text(']'))
|
||
|
# take opener off brackets stack
|
||
|
self.removeBracket()
|
||
|
return True
|
||
|
|
||
|
# If we got here, opener is a potential opener
|
||
|
is_image = opener.get('image')
|
||
|
|
||
|
# Check to see if we have a link/image
|
||
|
|
||
|
savepos = self.pos
|
||
|
|
||
|
# Inline link?
|
||
|
if self.peek() == '(':
|
||
|
self.pos += 1
|
||
|
self.spnl()
|
||
|
dest = self.parseLinkDestination()
|
||
|
if dest is not None and self.spnl():
|
||
|
# make sure there's a space before the title
|
||
|
if re.search(reWhitespaceChar, self.subject[self.pos-1]):
|
||
|
title = self.parseLinkTitle()
|
||
|
if self.spnl() and self.peek() == ')':
|
||
|
self.pos += 1
|
||
|
matched = True
|
||
|
else:
|
||
|
self.pos = savepos
|
||
|
|
||
|
if not matched:
|
||
|
# Next, see if there's a link label
|
||
|
beforelabel = self.pos
|
||
|
n = self.parseLinkLabel()
|
||
|
if n > 2:
|
||
|
reflabel = self.subject[beforelabel:beforelabel + n]
|
||
|
elif not opener.get('bracket_after'):
|
||
|
# Empty or missing second label means to use the first
|
||
|
# label as the reference. The reference must not
|
||
|
# contain a bracket. If we know there's a bracket, we
|
||
|
# don't even bother checking it.
|
||
|
reflabel = self.subject[opener.get('index'):startpos]
|
||
|
if n == 0:
|
||
|
# If shortcut reference link, rewind before spaces we skipped.
|
||
|
self.pos = savepos
|
||
|
|
||
|
if reflabel:
|
||
|
# lookup rawlabel in refmap
|
||
|
link = self.refmap.get(normalize_reference(reflabel))
|
||
|
if link:
|
||
|
dest = link['destination']
|
||
|
title = link['title']
|
||
|
matched = True
|
||
|
|
||
|
if matched:
|
||
|
node = Node('image' if is_image else 'link', None)
|
||
|
|
||
|
node.destination = dest
|
||
|
node.title = title or ''
|
||
|
tmp = opener.get('node').nxt
|
||
|
while tmp:
|
||
|
nxt = tmp.nxt
|
||
|
tmp.unlink()
|
||
|
node.append_child(tmp)
|
||
|
tmp = nxt
|
||
|
block.append_child(node)
|
||
|
self.processEmphasis(opener.get('previousDelimiter'))
|
||
|
self.removeBracket()
|
||
|
opener.get('node').unlink()
|
||
|
|
||
|
# We remove this bracket and processEmphasis will remove
|
||
|
# later delimiters.
|
||
|
# Now, for a link, we also deactivate earlier link openers.
|
||
|
# (no links in links)
|
||
|
if not is_image:
|
||
|
opener = self.brackets
|
||
|
while opener is not None:
|
||
|
if not opener.get('image'):
|
||
|
# deactivate this opener
|
||
|
opener['active'] = False
|
||
|
opener = opener.get('previous')
|
||
|
|
||
|
return True
|
||
|
else:
|
||
|
# no match
|
||
|
# remove this opener from stack
|
||
|
self.removeBracket()
|
||
|
self.pos = startpos
|
||
|
block.append_child(text(']'))
|
||
|
return True
|
||
|
|
||
|
def addBracket(self, node, index, image):
|
||
|
if self.brackets is not None:
|
||
|
self.brackets['bracketAfter'] = True
|
||
|
|
||
|
self.brackets = {
|
||
|
'node': node,
|
||
|
'previous': self.brackets,
|
||
|
'previousDelimiter': self.delimiters,
|
||
|
'index': index,
|
||
|
'image': image,
|
||
|
'active': True,
|
||
|
}
|
||
|
|
||
|
def removeBracket(self):
|
||
|
self.brackets = self.brackets.get('previous')
|
||
|
|
||
|
def parseEntity(self, block):
|
||
|
"""Attempt to parse an entity."""
|
||
|
m = self.match(reEntityHere)
|
||
|
if m:
|
||
|
block.append_child(text(HTMLunescape(m)))
|
||
|
return True
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
def parseString(self, block):
|
||
|
"""
|
||
|
Parse a run of ordinary characters, or a single character with
|
||
|
a special meaning in markdown, as a plain string.
|
||
|
"""
|
||
|
m = self.match(reMain)
|
||
|
if m:
|
||
|
if self.options.get('smart'):
|
||
|
s = re.sub(reEllipses, '\u2026', m)
|
||
|
s = re.sub(reDash, lambda x: smart_dashes(x.group()), s)
|
||
|
block.append_child(text(s))
|
||
|
else:
|
||
|
block.append_child(text(m))
|
||
|
return True
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
def parseNewline(self, block):
|
||
|
"""
|
||
|
Parse a newline. If it was preceded by two spaces, return a hard
|
||
|
line break; otherwise a soft line break.
|
||
|
"""
|
||
|
# assume we're at a \n
|
||
|
self.pos += 1
|
||
|
lastc = block.last_child
|
||
|
if lastc and lastc.t == 'text' and lastc.literal[-1] == ' ':
|
||
|
linebreak = len(lastc.literal) >= 2 and lastc.literal[-2] == ' '
|
||
|
lastc.literal = re.sub(reFinalSpace, '', lastc.literal)
|
||
|
if linebreak:
|
||
|
node = Node('linebreak', None)
|
||
|
else:
|
||
|
node = Node('softbreak', None)
|
||
|
block.append_child(node)
|
||
|
else:
|
||
|
block.append_child(Node('softbreak', None))
|
||
|
|
||
|
# gobble leading spaces in next line
|
||
|
self.match(reInitialSpace)
|
||
|
return True
|
||
|
|
||
|
def parseReference(self, s, refmap):
|
||
|
"""Attempt to parse a link reference, modifying refmap."""
|
||
|
self.subject = s
|
||
|
self.pos = 0
|
||
|
startpos = self.pos
|
||
|
|
||
|
# label:
|
||
|
match_chars = self.parseLinkLabel()
|
||
|
if match_chars == 0 or match_chars == 2:
|
||
|
return 0
|
||
|
else:
|
||
|
rawlabel = self.subject[:match_chars]
|
||
|
|
||
|
# colon:
|
||
|
if (self.peek() == ':'):
|
||
|
self.pos += 1
|
||
|
else:
|
||
|
self.pos = startpos
|
||
|
return 0
|
||
|
|
||
|
# link url
|
||
|
self.spnl()
|
||
|
|
||
|
dest = self.parseLinkDestination()
|
||
|
if dest is None:
|
||
|
self.pos = startpos
|
||
|
return 0
|
||
|
|
||
|
beforetitle = self.pos
|
||
|
self.spnl()
|
||
|
title = None
|
||
|
if self.pos != beforetitle:
|
||
|
title = self.parseLinkTitle()
|
||
|
if title is None:
|
||
|
title = ''
|
||
|
# rewind before spaces
|
||
|
self.pos = beforetitle
|
||
|
|
||
|
# make sure we're at line end:
|
||
|
at_line_end = True
|
||
|
if self.match(reSpaceAtEndOfLine) is None:
|
||
|
if title == '':
|
||
|
at_line_end = False
|
||
|
else:
|
||
|
# the potential title we found is not at the line end,
|
||
|
# but it could still be a legal link reference if we
|
||
|
# discard the title
|
||
|
title == ''
|
||
|
# rewind before spaces
|
||
|
self.pos = beforetitle
|
||
|
# and instead check if the link URL is at the line end
|
||
|
at_line_end = self.match(reSpaceAtEndOfLine) is not None
|
||
|
|
||
|
if not at_line_end:
|
||
|
self.pos = startpos
|
||
|
return 0
|
||
|
|
||
|
normlabel = normalize_reference(rawlabel)
|
||
|
if normlabel == '':
|
||
|
# label must contain non-whitespace characters
|
||
|
self.pos = startpos
|
||
|
return 0
|
||
|
|
||
|
if not refmap.get(normlabel):
|
||
|
refmap[normlabel] = {
|
||
|
'destination': dest,
|
||
|
'title': title
|
||
|
}
|
||
|
return (self.pos - startpos)
|
||
|
|
||
|
def parseInline(self, block):
|
||
|
"""
|
||
|
Parse the next inline element in subject, advancing subject
|
||
|
position.
|
||
|
|
||
|
On success, add the result to block's children and return True.
|
||
|
On failure, return False.
|
||
|
"""
|
||
|
res = False
|
||
|
c = self.peek()
|
||
|
if c is None:
|
||
|
return False
|
||
|
if c == '\n':
|
||
|
res = self.parseNewline(block)
|
||
|
elif c == '\\':
|
||
|
res = self.parseBackslash(block)
|
||
|
elif c == '`':
|
||
|
res = self.parseBackticks(block)
|
||
|
elif c == '*' or c == '_':
|
||
|
res = self.handleDelim(c, block)
|
||
|
elif c == "'" or c == '"':
|
||
|
res = self.options.get('smart') and self.handleDelim(c, block)
|
||
|
elif c == '[':
|
||
|
res = self.parseOpenBracket(block)
|
||
|
elif c == '!':
|
||
|
res = self.parseBang(block)
|
||
|
elif c == ']':
|
||
|
res = self.parseCloseBracket(block)
|
||
|
elif c == '<':
|
||
|
res = self.parseAutolink(block) or self.parseHtmlTag(block)
|
||
|
elif c == '&':
|
||
|
res = self.parseEntity(block)
|
||
|
else:
|
||
|
res = self.parseString(block)
|
||
|
|
||
|
if not res:
|
||
|
self.pos += 1
|
||
|
block.append_child(text(c))
|
||
|
|
||
|
return True
|
||
|
|
||
|
def parseInlines(self, block):
|
||
|
"""
|
||
|
Parse string content in block into inline children,
|
||
|
using refmap to resolve references.
|
||
|
"""
|
||
|
self.subject = block.string_content.strip()
|
||
|
self.pos = 0
|
||
|
self.delimiters = None
|
||
|
self.brackets = None
|
||
|
while (self.parseInline(block)):
|
||
|
pass
|
||
|
# allow raw string to be garbage collected
|
||
|
block.string_content = None
|
||
|
self.processEmphasis(None)
|
||
|
|
||
|
parse = parseInlines
|