Repository: Python-Markdown/markdown (python)

Issue:
attr_list ext strips curly braces from table cells
Curly braces in the elements of a table get stripped away when the 'extra' extension is loaded

```python
#! /usr/bin/env python3
import markdown
from markdown.extensions.extra import ExtraExtension
from markdown.extensions.tables import TableExtension

text = '''
Try {}

A  |  B
---|---
a{}| b{}

'''

extra = ExtraExtension()
tables= TableExtension()

md = markdown.Markdown(extensions=[tables])
print( md.convert(text) ) # this is fine

print("\n\n")

md = markdown.Markdown(extensions=[tables, extra])
print( md.convert(text) ) # curly braces in the table are stripped
```

The output (comments added manually):
```
<p>Try {}</p>
<table>
<thead>
<tr>
<th>A</th>
<th>B</th>
</tr>
</thead>
<tbody>
<tr>
<td>a{}</td>  -- braces are there
<td>b{}</td>
</tr>
</tbody>
</table>



<p>Try {}</p>  -- these braces are preserved
<table>
<thead>
<tr>
<th>A</th>
<th>B</th>
</tr>
</thead>
<tbody>
<tr>
<td>a</td>  -- braces gone
<td>b</td>
</tr>
</tbody>
</table>
```

Is it an intended behaviour?

Pull request description:
Tune attr list regex
Ignore empty braces. Braces must contain at least one non-whitepsace
character to be recognized as an attr list.

Attr lists for table cells must be at the end of the cell content and must
be seperated from the content by at least one space. This appears to be
a breaking change. However, it is consistent with the behavior elsewhere.

Fixes #898.

Patch:
diff --git a/docs/change_log/release-3.3.md b/docs/change_log/release-3.3.md
index c29ddcc..0e0bb89 100644
--- a/docs/change_log/release-3.3.md
+++ b/docs/change_log/release-3.3.md
@@ -31,6 +31,23 @@ markdown.markdown(src, extensions=[FencedCodeExtension(lang_prefix='')])
     provide an option to include the language class in the output, let alone prefix it. Therefore, any language prefix
     is only applied when syntax highlighting is disabled.
 
+### Attribute Lists are more strict (#898).
+
+Empty curly braces are now completely ignored by the [Attribute List] extension. Previously, the extension would
+recognize them as attribute lists and remove them from the document. Therefore, it is no longer necessary to backslash
+escape a set of curly braces which are empty or only contain whitespace.
+
+Despite not being documented, previously an attribute list could be defined anywhere within a table cell and get
+applied to the cell (`<td>` element). Now the attribute list must be defined at the end of the cell content and must
+be separated from the rest of the content by at least one space. This makes it easy to differentiate between attribute
+lists defined on inline elements within a cell and the attribute list for the cell itself. It is also more consistent
+with how attribute lists are defined on other types of elements.
+
+The extension has also added support for defining attribute lists on table header cells (`<th>` elements) in the same
+manner as data cells (`<td>` elements).
+
+In addition, the documentation for the extensions received an overhaul. The features (#987) and limitations (#965) of the extension are now fully documented.
+
 ## New features
 
 The following new features have been included in the 3.3 release:
@@ -55,10 +72,10 @@ The following bug fixes are included in the 3.3 release:
 
 * Avoid a `RecursionError` from deeply nested blockquotes (#799).
 * Fix issues with complex emphasis (#979).
-* Limitations of `attr_list` extension are Documented (#965).
 * Fix unescaping of HTML characters `<>` in CodeHilite (#990).
 
 [spec]: https://www.w3.org/TR/html5/text-level-semantics.html#the-code-element
 [fenced_code]: ../extensions/fenced_code_blocks.md
 [codehilite]: ../extensions/code_hilite.md
 [enabled]: ../extensions/fenced_code_blocks.md#enabling-syntax-highlighting
+[Attribute List]: ../extensions/attr_list.md
diff --git a/docs/extensions/attr_list.md b/docs/extensions/attr_list.md
index d2dd0d2..d3d5e12 100644
--- a/docs/extensions/attr_list.md
+++ b/docs/extensions/attr_list.md
@@ -53,6 +53,9 @@ Curly braces can be backslash escaped to avoid being identified as an attribute
 \{ not an attribute list }
 ```
 
+Opening and closing curly braces which are empty or only contain whitespace are ignored whether they are escaped or
+not. Additionally, any attribute lists which are not located in the specific locations documented below are ignored.
+
 The colon after the opening brace is optional, but is supported to maintain consistency with other implementations.
 Therefore, the following is also a valid attribute list:
 
@@ -119,6 +122,42 @@ The above results in the following output:
 <p><a href="http://example.com" class="foo bar" title="Some title!">link</a></p>
 ```
 
+If the [tables](./tables.md) extension is enabled, attribute lists can be defined on table cells. To differentiate
+attributes for an inline element from attributes for the containing cell, the attribute list must be separated from
+the content by at least one space and be defined at the end of the cell content. As table cells can only ever be on
+a single line, the attribute list must remain on the same line as the content of the cell.
+
+```text
+| set on td    | set on em   |
+|--------------|-------------|
+| *a* { .foo } | *b*{ .foo } |
+```
+
+The above example results in the following output:
+
+```html
+<table>
+  <thead>
+    <tr>
+      <th>set on td</th>
+      <th>set on em</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td class="foo"><em>a</em></td>
+      <td><em class="foo">b</em></td>
+    </tr>
+  </tbody>
+</table>
+```
+
+Note that in the first column, the attribute list is preceded by a space; therefore, it is assigned to the table cell
+(`<td>` element). However, in the second column, the attribute list is not preceded by a space; therefore, it is
+assigned to the inline element (`<em>`) which immediately preceded it.
+
+Attribute lists may also be defined on table header cells (`<th>` elements) in the same manner.
+
 ### Limitations
 
 There are a few types of elements which attribute lists do not work with. As a reminder, Markdown is a subset of HTML
@@ -147,7 +186,7 @@ __Implied Elements:__
 :   There are various HTML elements which are not represented in Markdown text, but only implied. For example, the
     `ul` and `ol` elements do not exist in Markdown. They are only implied by the presence of list items (`li`). There
     is no way to use an attribute list to define attributes on implied elements, including but not limited to the
-    following: `ul`, `ol`, `dl`, `table`, `thead`, `tbody`, `tr`, and `th`.
+    following: `ul`, `ol`, `dl`, `table`, `thead`, `tbody`, and `tr`.
 
 ## Usage
 
diff --git a/docs/extensions/tables.md b/docs/extensions/tables.md
index 70b51c0..30b7636 100644
--- a/docs/extensions/tables.md
+++ b/docs/extensions/tables.md
@@ -49,6 +49,9 @@ will be rendered as:
 </table>
 ```
 
+!!! seealso "See Also"
+    The [Attribute Lists](./attr_list.md) extension includes support for defining attributes on table cells.
+
 Usage
 -----
 
diff --git a/markdown/extensions/attr_list.py b/markdown/extensions/attr_list.py
index 2a39fc0..9a67551 100644
--- a/markdown/extensions/attr_list.py
+++ b/markdown/extensions/attr_list.py
@@ -64,10 +64,10 @@ def isheader(elem):
 
 class AttrListTreeprocessor(Treeprocessor):
 
-    BASE_RE = r'\{\:?([^\}\n]*)\}'
-    HEADER_RE = re.compile(r'[ ]+%s[ ]*$' % BASE_RE)
-    BLOCK_RE = re.compile(r'\n[ ]*%s[ ]*$' % BASE_RE)
-    INLINE_RE = re.compile(r'^%s' % BASE_RE)
+    BASE_RE = r'\{\:?[ ]*([^\}\n ][^\}\n]*)[ ]*\}'
+    HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE))
+    BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(BASE_RE))
+    INLINE_RE = re.compile(r'^{}'.format(BASE_RE))
     NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff'
                          r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d'
                          r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff'
@@ -79,8 +79,8 @@ class AttrListTreeprocessor(Treeprocessor):
             if self.md.is_block_level(elem.tag):
                 # Block level: check for attrs on last line of text
                 RE = self.BLOCK_RE
-                if isheader(elem) or elem.tag == 'dt':
-                    # header or def-term: check for attrs at end of line
+                if isheader(elem) or elem.tag in ['dt', 'td', 'th']:
+                    # header, def-term, or table cell: check for attrs at end of element
                     RE = self.HEADER_RE
                 if len(elem) and elem.tag == 'li':
                     # special case list items. children may include a ul or ol.
@@ -120,8 +120,6 @@ class AttrListTreeprocessor(Treeprocessor):
                 elif elem.text:
                     # no children. Get from text.
                     m = RE.search(elem.text)
-                    if not m and elem.tag == 'td':
-                        m = re.search(self.BASE_RE, elem.text)
                     if m:
                         self.assign_attrs(elem, m.group(1))
                         elem.text = elem.text[:m.start()]


Test patch:
diff --git a/tests/extensions/extra/tables_and_attr_list.html b/tests/extensions/extra/tables_and_attr_list.html
deleted file mode 100644
index 1924389..0000000
--- a/tests/extensions/extra/tables_and_attr_list.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<table>
-<thead>
-<tr>
-<th>First Header</th>
-<th>Second Header</th>
-</tr>
-</thead>
-<tbody>
-<tr>
-<td class="foo bar" title="Some title!">Content Cell</td>
-<td>Content Cell</td>
-</tr>
-<tr>
-<td>Content Cell</td>
-<td class="foo bar" title="Some title!">Content Cell</td>
-</tr>
-</tbody>
-</table>
\ No newline at end of file
diff --git a/tests/extensions/extra/tables_and_attr_list.txt b/tests/extensions/extra/tables_and_attr_list.txt
deleted file mode 100644
index 517eeeb..0000000
--- a/tests/extensions/extra/tables_and_attr_list.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-First Header                                           | Second Header
------------------------------------------------------- | -------------
-Content Cell{: class="foo bar" title="Some title!" }   | Content Cell
-Content Cell                                           | Content Cell{: class="foo bar" title="Some title!" }
diff --git a/tests/test_legacy.py b/tests/test_legacy.py
index a3495a0..94fdd13 100644
--- a/tests/test_legacy.py
+++ b/tests/test_legacy.py
@@ -184,8 +184,6 @@ class TestExtensionsExtra(LegacyTestCase):
 
     tables = Kwargs(extensions=['tables'])
 
-    tables_and_attr_list = Kwargs(extensions=['tables', 'attr_list'])
-
     extra_config = Kwargs(
         extensions=['extra'],
         extension_configs={
diff --git a/tests/test_syntax/extensions/test_attr_list.py b/tests/test_syntax/extensions/test_attr_list.py
new file mode 100644
index 0000000..6baaafb
--- /dev/null
+++ b/tests/test_syntax/extensions/test_attr_list.py
@@ -0,0 +1,80 @@
+"""
+Python Markdown
+
+A Python implementation of John Gruber's Markdown.
+
+Documentation: https://python-markdown.github.io/
+GitHub: https://github.com/Python-Markdown/markdown/
+PyPI: https://pypi.org/project/Markdown/
+
+Started by Manfred Stienstra (http://www.dwerg.net/).
+Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
+Currently maintained by Waylan Limberg (https://github.com/waylan),
+Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
+
+Copyright 2007-2020 The Python Markdown Project (v. 1.7 and later)
+Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
+Copyright 2004 Manfred Stienstra (the original version)
+
+License: BSD (see LICENSE.md for details).
+"""
+
+from markdown.test_tools import TestCase
+
+
+class TestAttrList(TestCase):
+
+    maxDiff = None
+
+    # TODO: Move the rest of the attr_list tests here.
+
+    def test_empty_list(self):
+        self.assertMarkdownRenders(
+            '*foo*{ }',
+            '<p><em>foo</em>{ }</p>',
+            extensions=['attr_list']
+        )
+
+    def test_table_td(self):
+        self.assertMarkdownRenders(
+            self.dedent(
+                """
+                | A { .foo }  | *B*{ .foo } | C { } | D{ .foo }     | E { .foo } F |
+                |-------------|-------------|-------|---------------|--------------|
+                | a { .foo }  | *b*{ .foo } | c { } | d{ .foo }     | e { .foo } f |
+                | valid on td | inline      | empty | missing space | not at end   |
+                """
+            ),
+            self.dedent(
+                """
+                <table>
+                <thead>
+                <tr>
+                <th class="foo">A</th>
+                <th><em class="foo">B</em></th>
+                <th>C { }</th>
+                <th>D{ .foo }</th>
+                <th>E { .foo } F</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr>
+                <td class="foo">a</td>
+                <td><em class="foo">b</em></td>
+                <td>c { }</td>
+                <td>d{ .foo }</td>
+                <td>e { .foo } f</td>
+                </tr>
+                <tr>
+                <td>valid on td</td>
+                <td>inline</td>
+                <td>empty</td>
+                <td>missing space</td>
+                <td>not at end</td>
+                </tr>
+                </tbody>
+                </table>
+                """
+            ),
+            extensions=['attr_list', 'tables']
+        )