From 5598a88c360877a5747e2ab9d19d387a01e1385a Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 10 Jan 2017 12:49:45 -0700 Subject: [PATCH 1/5] Handle backtics sanely in tables At some point the logic of counting backtics and determining if they are odd or even was used to parse a row's text into cells. Unfortunately this approach broke expected code parsing logic in a table. We essentially traded one bug for another. This fixes table backtick handling and restores sane backtick logic while preserving existing fixes. --- markdown/extensions/tables.py | 103 ++++++++++++++++++----------- tests/extensions/extra/tables.html | 17 ++++- tests/extensions/extra/tables.txt | 6 ++ 3 files changed, 85 insertions(+), 41 deletions(-) diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py index 7c755074d..83d7d3a82 100644 --- a/markdown/extensions/tables.py +++ b/markdown/extensions/tables.py @@ -19,13 +19,15 @@ from __future__ import unicode_literals from . import Extension from ..blockprocessors import BlockProcessor -from ..inlinepatterns import BacktickPattern, BACKTICK_RE from ..util import etree +import re class TableProcessor(BlockProcessor): """ Process Tables. """ + RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`)|(`+)|(\\\|)|(\|))') + def test(self, parent, block): rows = block.split('\n') return (len(rows) > 1 and '|' in rows[0] and @@ -88,50 +90,71 @@ def _split_row(self, row, border): row = row[1:] if row.endswith('|'): row = row[:-1] - return self._split(row, '|') + return self._split(row) - def _split(self, row, marker): + def _split(self, row): """ split a row of text with some code into a list of cells. """ - if self._row_has_unpaired_backticks(row): - # fallback on old behaviour - return row.split(marker) - # modify the backtick pattern to only match at the beginning of the search string - backtick_pattern = BacktickPattern('^' + BACKTICK_RE) + elements = [] - current = '' - i = 0 - while i < len(row): - letter = row[i] - if letter == marker: - if current != '' or len(elements) == 0: - # Don't append empty string unless it is the first element - # The border is already removed when we get the row, then the line is strip()'d - # If the first element is a marker, then we have an empty first cell - elements.append(current) - current = '' - else: - match = backtick_pattern.getCompiledRegExp().match(row[i:]) - if not match: - current += letter - else: - groups = match.groups() - delim = groups[1] # the code block delimeter (ie 1 or more backticks) - row_contents = groups[2] # the text contained inside the code block - i += match.start(4) - 1 # jump pointer to the beginning of the rest of the text (group #4) - element = delim + row_contents + delim # reinstert backticks - current += element - i += 1 - elements.append(current) + pipes = [] + tics = [] + tic_points = [] + tic_region = [] + good_pipes = [] + + # Parse row + # Throw out \\, \`, and \| + for m in self.RE_CODE_PIPES.finditer(row): + # Store ` data (len, start_pos, end_pos) + if m.group(3): + # `+ + # Store length of each tic group + tics.append(len(m.group(3))) + # Store start and end of tic group + tic_points.append((m.start(3), m.end(3) - 1)) + # Store pipe location + elif m.group(5): + pipes.append(m.start(5)) + + # Pair up tics according to size if possible + # Walk through tic list and see if tic has a close. + # Store the tic region (start of region, end of region). + pos = 0 + tic_len = len(tics) + while pos < tic_len: + try: + index = tics[pos + 1:].index(tics[pos]) + 1 + tic_region.append((tic_points[pos][0], tic_points[pos + index][1])) + pos += index + 1 + except ValueError: + pos += 1 + + # Resolve pipes. Check if they are within a tic pair region. + # Walk through pipes comparing them to each region. + # - If pipe position is less that a region, it isn't in a region + # - If it is within a region, we don't want it, so throw it out + # - If we didn't throw it out, it must be a table pipe + for pipe in pipes: + throw_out = False + for region in tic_region: + if pipe < region[0]: + # Pipe is not in a region + break + elif region[0] <= pipe <= region[1]: + # Pipe is within a code region. Throw it out. + throw_out = True + break + if not throw_out: + good_pipes.append(pipe) + + # Split row according to table delimeters. + pos = 0 + for pipe in good_pipes: + elements.append(row[pos:pipe]) + pos = pipe + 1 + elements.append(row[pos:]) return elements - def _row_has_unpaired_backticks(self, row): - count_total_backtick = row.count('`') - count_escaped_backtick = row.count('\`') - count_backtick = count_total_backtick - count_escaped_backtick - # odd number of backticks, - # we won't be able to build correct code blocks - return count_backtick & 1 - class TableExtension(Extension): """ Add tables to Markdown. """ diff --git a/tests/extensions/extra/tables.html b/tests/extensions/extra/tables.html index 85d994036..ce4558b7d 100644 --- a/tests/extensions/extra/tables.html +++ b/tests/extensions/extra/tables.html @@ -255,4 +255,19 @@

Table Tests

\ No newline at end of file + +

A test for issue #449

+ + + + + + + + + + + + + +
Odd backticsEven backtics
[!\"\#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~][!\"\#$%&'()*+,\-./:;<=>?@\[\\\]^`_`{|}~]
\ No newline at end of file diff --git a/tests/extensions/extra/tables.txt b/tests/extensions/extra/tables.txt index c84391893..eeaa8a233 100644 --- a/tests/extensions/extra/tables.txt +++ b/tests/extensions/extra/tables.txt @@ -80,3 +80,9 @@ Lists are not tables - this | should | not - be | a | table + +A test for issue #449 + +Odd backtics | Even backtics +------------ | ------------- +``[!\"\#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~]`` | ``[!\"\#$%&'()*+,\-./:;<=>?@\[\\\]^`_`{|}~]`` From a6ab87f20117847ede5ba9218201a916d1fb895d Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 10 Jan 2017 12:52:15 -0700 Subject: [PATCH 2/5] Fix silent failing of spellcheck Travis should checkout aspell-en --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 0038bdced..1badcef44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ addons: packages: - libtidy-0.99-0 - aspell + - aspell-en install: - pip install tox - pip install coveralls From 40ce6589da892c7d803616d1907c374b9d7d9132 Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 10 Jan 2017 13:04:17 -0700 Subject: [PATCH 3/5] Match spacing after docstring to reset of file --- markdown/extensions/tables.py | 1 - 1 file changed, 1 deletion(-) diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py index 83d7d3a82..e94d9f84b 100644 --- a/markdown/extensions/tables.py +++ b/markdown/extensions/tables.py @@ -94,7 +94,6 @@ def _split_row(self, row, border): def _split(self, row): """ split a row of text with some code into a list of cells. """ - elements = [] pipes = [] tics = [] From a221446fc34d5dabaea6a6fe7bbc0891fe437a76 Mon Sep 17 00:00:00 2001 From: Isaac Date: Tue, 10 Jan 2017 17:18:29 -0700 Subject: [PATCH 4/5] Backslash work different with backtics One thing got overlooked. Backslashes don't quite work the same on backtics. To keep the same feel as code outside the table, we need to account for backslash and backtics the same. --- markdown/extensions/tables.py | 10 ++++++++-- tests/extensions/extra/tables.html | 16 +++++++++++++++- tests/extensions/extra/tables.txt | 6 +++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py index e94d9f84b..5600e8355 100644 --- a/markdown/extensions/tables.py +++ b/markdown/extensions/tables.py @@ -26,7 +26,7 @@ class TableProcessor(BlockProcessor): """ Process Tables. """ - RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`)|(`+)|(\\\|)|(\|))') + RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`+)|(`+)|(\\\|)|(\|))') def test(self, parent, block): rows = block.split('\n') @@ -105,7 +105,13 @@ def _split(self, row): # Throw out \\, \`, and \| for m in self.RE_CODE_PIPES.finditer(row): # Store ` data (len, start_pos, end_pos) - if m.group(3): + if m.group(2): + # \`+ + # Store length of each tic group + tics.append(len(m.group(2)) - 1) + # Store start and end of tic group + tic_points.append((m.start(3), m.end(3) - 1)) + elif m.group(3): # `+ # Store length of each tic group tics.append(len(m.group(3))) diff --git a/tests/extensions/extra/tables.html b/tests/extensions/extra/tables.html index ce4558b7d..7cfb4e42a 100644 --- a/tests/extensions/extra/tables.html +++ b/tests/extensions/extra/tables.html @@ -256,7 +256,7 @@

Table Tests

  • this | should | not
  • be | a | table
  • -

    A test for issue #449

    +

    Add tests for issue #449

    @@ -270,4 +270,18 @@

    Table Tests

    +
    [!\"\#$%&'()*+,\-./:;<=>?@\[\\\]^`_`{|}~]
    + + + + + + + + + + + + +
    EscapesMore Escapes
    `\\
    \ No newline at end of file diff --git a/tests/extensions/extra/tables.txt b/tests/extensions/extra/tables.txt index eeaa8a233..fd9d9fb9b 100644 --- a/tests/extensions/extra/tables.txt +++ b/tests/extensions/extra/tables.txt @@ -81,8 +81,12 @@ Lists are not tables - this | should | not - be | a | table -A test for issue #449 +Add tests for issue #449 Odd backtics | Even backtics ------------ | ------------- ``[!\"\#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~]`` | ``[!\"\#$%&'()*+,\-./:;<=>?@\[\\\]^`_`{|}~]`` + +Escapes | More Escapes +------- | ------ +`` `\`` | `\` From dc564bbf346979639d84377cc8d9afd48018a3da Mon Sep 17 00:00:00 2001 From: facelessuser Date: Tue, 10 Jan 2017 17:55:39 -0700 Subject: [PATCH 5/5] Clean up table backtic code --- markdown/extensions/tables.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py index 5600e8355..4bb20767e 100644 --- a/markdown/extensions/tables.py +++ b/markdown/extensions/tables.py @@ -26,7 +26,7 @@ class TableProcessor(BlockProcessor): """ Process Tables. """ - RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`+)|(`+)|(\\\|)|(\|))') + RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(`+)|(\\\|)|(\|))') def test(self, parent, block): rows = block.split('\n') @@ -102,24 +102,18 @@ def _split(self, row): good_pipes = [] # Parse row - # Throw out \\, \`, and \| + # Throw out \\, and \| for m in self.RE_CODE_PIPES.finditer(row): # Store ` data (len, start_pos, end_pos) if m.group(2): - # \`+ - # Store length of each tic group - tics.append(len(m.group(2)) - 1) - # Store start and end of tic group - tic_points.append((m.start(3), m.end(3) - 1)) - elif m.group(3): # `+ # Store length of each tic group - tics.append(len(m.group(3))) + tics.append(len(m.group(2))) # Store start and end of tic group - tic_points.append((m.start(3), m.end(3) - 1)) + tic_points.append((m.start(2), m.end(2) - 1)) # Store pipe location - elif m.group(5): - pipes.append(m.start(5)) + elif m.group(4): + pipes.append(m.start(4)) # Pair up tics according to size if possible # Walk through tic list and see if tic has a close.