Skip to content

Commit

Permalink
Upgrade to 4.10.0
Browse files Browse the repository at this point in the history
  • Loading branch information
facelessuser committed Apr 21, 2018
1 parent 11633ec commit 25c9f4f
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 81 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
PyMdown Extensions for Sublime Text

Current version: 4.9.2
Current version: 4.10.0
2 changes: 1 addition & 1 deletion st3/pymdownx/__version__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Version."""

# (major, minor, micro, release type, pre-release build, post-release build)
version_info = (4, 9, 2, 'final', 0, 0)
version_info = (4, 10, 0, 'final', 0, 0)


def _version():
Expand Down
222 changes: 143 additions & 79 deletions st3/pymdownx/superfences.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,42 +37,30 @@
from . import highlight as hl
import re

NESTED_FENCE_START = r'''(?x)
(?:^(?P<ws>[\> ]*)(?P<fence>~{3,}|`{3,}))[ ]* # Fence opening
(\{? # Language opening
\.?(?P<lang>[\w#.+-]*))?[ ]* # Language
(?:
(hl_lines=(?P<quot>"|')(?P<hl_lines>\d+(?:[ ]+\d+)*)(?P=quot))?[ ]*| # highlight lines
(linenums=(?P<quot2>"|') # Line numbers
(?P<linestart>[\d]+) # Line number start
(?:[ ]+(?P<linestep>[\d]+))? # Line step
(?:[ ]+(?P<linespecial>[\d]+))? # Line special
(?P=quot2))?[ ]*
){,2}
}?[ ]*$ # Language closing
'''
NESTED_FENCE_END = r'^[\> ]*%s[ ]*$'

WS = r'^([\> ]{0,%d})(.*)'

RE_FENCE = re.compile(
r'''(?xsm)
(?P<fence>^(?:~{3,}|`{3,}))[ ]* # Opening
(\{?\.?(?P<lang>[\w#.+-]*))?[ ]* # Optional {, and lang
SOH = '\u0001'
EOT = '\u0004'

PREFIX_CHARS = ('>', ' ', '\t')

RE_NESTED_FENCE_START = re.compile(
r'''(?x)
(?P<fence>~{3,}|`{3,})[ \t]* # Fence opening
(\{? # Language opening
\.?(?P<lang>[\w#.+-]*))?[ \t]* # Language
(?:
(hl_lines=(?P<quot>"|')(?P<hl_lines>\d+(?:[ ]+\d+)*)(?P=quot))?[ ]*| # Optional highlight lines option
(linenums=(?P<quot2>"|') # Line numbers
(?P<linestart>[\d]+) # Line number start
(?:[ ]+(?P<linestep>[\d]+))? # Line step
(?:[ ]+(?P<linespecial>[\d]+))? # Line special
(?P=quot2))?[ ]*
(hl_lines=(?P<quot>"|')(?P<hl_lines>\d+(?:[ \t]+\d+)*)(?P=quot))?[ \t]*| # highlight lines
(linenums=(?P<quot2>"|') # Line numbers
(?P<linestart>[\d]+) # Line number start
(?:[ \t]+(?P<linestep>[\d]+))? # Line step
(?:[ \t]+(?P<linespecial>[\d]+))? # Line special
(?P=quot2))?[ \t]*
){,2}
}?[ ]*\n # Optional closing }
(?P<code>.*?)(?<=\n) # Code
(?P=fence)[ ]*$ # Closing
}?[ \t]*$ # Language closing
'''
)

NESTED_FENCE_END = r'%s[ \t]*$'


def _escape(txt):
"""Basic html escaping."""
Expand Down Expand Up @@ -159,7 +147,8 @@ def __init__(self, *args, **kwargs):
"Set class name for wrapper element. The default of CodeHilite or Highlight will be used"
"if nothing is set. - "
"Default: ''"
]
],
'preserve_tabs': [False, "Preserve tabs in fences - Default: False"]
}
super(SuperFencesCodeExtension, self).__init__(*args, **kwargs)

Expand Down Expand Up @@ -226,7 +215,12 @@ def patch_fenced_rule(self):
indented_code.extension = self
self.superfences[0]["formatter"] = fenced.highlight
self.markdown.parser.blockprocessors['code'] = indented_code
self.markdown.preprocessors.add('fenced_code_block', fenced, ">normalize_whitespace")
if config["preserve_tabs"]:
self.markdown.preprocessors.add('fenced_code_block', fenced, "<normalize_whitespace")
post_fenced = SuperFencesBlockPostNormalizePreprocessor(self.markdown)
self.markdown.preprocessors.add('fenced_code_post_norm', post_fenced, ">normalize_whitespace")
else:
self.markdown.preprocessors.add('fenced_code_block', fenced, ">normalize_whitespace")

def reset(self):
"""Clear the stash."""
Expand All @@ -235,6 +229,28 @@ def reset(self):
entry["stash"].clear_stash()


class SuperFencesBlockPostNormalizePreprocessor(Preprocessor):
"""Preprocessor to clean up normalization bypass hack."""

TEMP_PLACEHOLDER_RE = re.compile(
r'^([\> ]*)%s(%s)%s$' % (
SOH,
md_util.HTML_PLACEHOLDER[1:-1] % r'([0-9]+)',
EOT
)
)

def run(self, lines):
"""Search for fenced blocks."""

new_lines = []
for line in lines:
line = self.TEMP_PLACEHOLDER_RE.sub(r'\1' + md_util.STX + r'\2' + md_util.ETX, line)
new_lines.append(line)

return new_lines


class SuperFencesBlockPreprocessor(Preprocessor):
"""
Preprocessor to find fenced code blocks.
Expand All @@ -244,21 +260,26 @@ class SuperFencesBlockPreprocessor(Preprocessor):
text from an indented code block.
"""

fence_start = re.compile(NESTED_FENCE_START)
CODE_WRAP = '<pre%s><code%s>%s</code></pre>'

def __init__(self, md):
"""Initialize."""

super(SuperFencesBlockPreprocessor, self).__init__(md)
self.markdown = md
self.tab_len = self.markdown.tab_length
self.checked_hl_settings = False
self.codehilite_conf = {}

def normalize_ws(self, text):
"""Normalize whitespace."""

return text.expandtabs(self.tab_len)

def rebuild_block(self, lines):
"""Dedent the fenced block lines."""

return '\n'.join([line[self.ws_len:] for line in lines])
return '\n'.join([line[self.ws_virtual_len:] for line in lines])

def get_hl_settings(self):
"""Check for CodeHilite extension to get its config."""
Expand All @@ -283,6 +304,7 @@ def clear(self):

self.ws = None
self.ws_len = 0
self.ws_virtual_len = 0
self.fence = None
self.lang = None
self.hl_lines = None
Expand All @@ -292,65 +314,64 @@ def clear(self):
self.quote_level = 0
self.code = []
self.empty_lines = 0
self.whitespace = None
self.fence_end = None

def eval(self, m, start, end):
def eval_fence(self, ws, content, start, end):
"""Evaluate a normal fence."""

if m.group(0).strip() == '':
if (ws + content).strip() == '':
# Empty line is okay
self.empty_lines += 1
self.code.append(m.group(0))
elif len(m.group(1)) != self.ws_len and m.group(2) != '':
self.code.append(ws + content)
elif len(ws) != self.ws_virtual_len and content != '':
# Not indented enough
self.clear()
elif self.fence_end.match(m.group(0)) is not None and not m.group(2).startswith(' '):
elif self.fence_end.match(content) is not None and not content.startswith((' ', '\t')):
# End of fence
self.process_nested_block(m, start, end)
self.process_nested_block(ws, content, start, end)
else:
# Content line
self.empty_lines = 0
self.code.append(m.group(0))
self.code.append(ws + content)

def eval_quoted(self, m, quote_level, start, end):
def eval_quoted(self, ws, content, quote_level, start, end):
"""Evaluate fence inside a blockquote."""

if quote_level > self.quote_level:
# Quote level exceeds the starting quote level
self.clear()
elif quote_level <= self.quote_level:
if m.group(2) == '':
if content == '':
# Empty line is okay
self.code.append(m.group(0))
self.code.append(ws + content)
self.empty_lines += 1
elif len(m.group(1)) < self.ws_len:
elif len(ws) < self.ws_len:
# Not indented enough
self.clear()
elif self.empty_lines and quote_level < self.quote_level:
# Quote levels don't match and we are signified
# the end of the block with an empty line
self.clear()
elif self.fence_end.match(m.group(0)) is not None:
elif self.fence_end.match(content) is not None:
# End of fence
self.process_nested_block(m, start, end)
self.process_nested_block(ws, content, start, end)
else:
# Content line
self.empty_lines = 0
self.code.append(m.group(0))
self.code.append(ws + content)

def process_nested_block(self, m, start, end):
def process_nested_block(self, ws, content, start, end):
"""Process the contents of the nested block."""

self.last = m.group(0)
self.last = ws + self.normalize_ws(content)
code = None
for entry in reversed(self.extension.superfences):
if entry["test"](self.lang):
code = entry["formatter"](self.rebuild_block(self.code), self.lang)
break

if code is not None:
self._store('\n'.join(self.code) + '\n', code, start, end, entry)
self._store(self.normalize_ws('\n'.join(self.code)) + '\n', code, start, end, entry)
self.clear()

def parse_hl_lines(self, hl_lines):
Expand All @@ -375,19 +396,62 @@ def parse_line_special(self, linespecial):

return int(linespecial) if linespecial else -1

def parse_fence_line(self, line):
"""Parse fence line."""

ws_len = 0
ws_virtual_len = 0
ws = []
index = 0
for c in line:
if ws_virtual_len >= self.ws_virtual_len:
break
if c not in PREFIX_CHARS:
break
ws_len += 1
if c == '\t':
tab_size = self.tab_len - (index % self.tab_len)
ws_virtual_len += tab_size
ws.append(' ' * tab_size)
else:
tab_size = 1
ws_virtual_len += 1
ws.append(c)
index += tab_size

return ''.join(ws), line[ws_len:]

def parse_whitespace(self, line):
"""Parse the whitespace (blockquote syntax is counted as well)."""

self.ws_len = 0
self.ws_virtual_len = 0
ws = []
for c in line:
if c not in PREFIX_CHARS:
break
self.ws_len += 1
ws.append(c)

ws = self.normalize_ws(''.join(ws))
self.ws_virtual_len = len(ws)

return ws

def search_nested(self, lines):
"""Search for nested fenced blocks."""

count = 0
for line in lines:
if self.fence is None:
ws = self.parse_whitespace(line)

# Found the start of a fenced block.
m = self.fence_start.match(line)
m = RE_NESTED_FENCE_START.match(line, self.ws_len)
if m is not None:
start = count
self.first = m.group(0)
self.ws = m.group('ws') if m.group('ws') else ''
self.ws_len = len(self.ws)
self.first = ws + self.normalize_ws(m.group(0))
self.ws = ws
self.quote_level = self.ws.count(">")
self.empty_lines = 0
self.fence = m.group('fence')
Expand All @@ -397,7 +461,6 @@ def search_nested(self, lines):
self.linestep = m.group('linestep')
self.linespecial = m.group('linespecial')
self.fence_end = re.compile(NESTED_FENCE_END % self.fence)
self.whitespace = re.compile(WS % self.ws_len)
else:
# Evaluate lines
# - Determine if it is the ending line or content line
Expand All @@ -407,33 +470,33 @@ def search_nested(self, lines):
# - When content lines are inside blockquotes, make sure
# the nested block quote levels make sense according to
# blockquote rules.
m = self.whitespace.match(line)
if m:
end = count + 1
quote_level = m.group(1).count(">")

if self.quote_level:
# Handle blockquotes
self.eval_quoted(m, quote_level, start, end)
elif quote_level == 0:
# Handle all other cases
self.eval(m, start, end)
else:
# Looks like we got a blockquote line
# when not in a blockquote.
self.clear()
else: # pragma: no cover
# I am 99.9999% sure we will never hit this line.
# But I am too chicken to pull it out :).
ws, content = self.parse_fence_line(line)

end = count + 1
quote_level = ws.count(">")

if self.quote_level:
# Handle blockquotes
self.eval_quoted(ws, content, quote_level, start, end)
elif quote_level == 0:
# Handle all other cases
self.eval_fence(ws, content, start, end)
else:
# Looks like we got a blockquote line
# when not in a blockquote.
self.clear()

count += 1

# Now that we are done iterating the lines,
# let's replace the original content with the
# fenced blocks.
while len(self.stack):
fenced, start, end = self.stack.pop()
lines = lines[:start] + [fenced] + lines[end:]
if self.preserve_tabs:
lines = lines[:start] + [fenced.replace(md_util.STX, SOH, 1)[:-1] + EOT] + lines[end:]
else:
lines = lines[:start] + [fenced] + lines[end:]
return lines

def highlight(self, src, language):
Expand Down Expand Up @@ -485,8 +548,8 @@ def _store(self, source, code, start, end, obj):
# we can restore the original source
obj["stash"].store(
placeholder[1:-1],
"%s\n%s%s" % (self.first, source, self.last),
self.ws_len
"%s\n%s%s" % (self.first, self.normalize_ws(source), self.last),
self.ws_virtual_len
)

def run(self, lines):
Expand All @@ -496,6 +559,7 @@ def run(self, lines):
self.clear()
self.stack = []
self.disabled_indented = self.config.get("disable_indented_code_blocks", False)
self.preserve_tabs = self.config.get("preserve_tabs", False)

lines = self.search_nested(lines)

Expand Down

0 comments on commit 25c9f4f

Please sign in to comment.