Skip to content

Commit 65244cc

Browse files
committed
Allow text-alignment and width props, passed via the style attribute
1 parent 60f408f commit 65244cc

File tree

3 files changed

+121
-11
lines changed

3 files changed

+121
-11
lines changed

src/wagtail_tinytableblock/static/wagtail_tinytableblock/js/tiny-table-block.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ class TinyTableBlockDefinition extends window.wagtailStreamField.blocks.FieldBlo
44
const block = super.render(placeholder, prefix, initialState, initialError);
55

66
let plugins = "table autoresize";
7-
let toolbar = "undo redo | tablerowheader tablecolheader tablemergecells tablesplitcells tablecellprops tablerowprops| tableinsertcolbefore tableinsertcolafter tableinsertrowbefore tableinsertrowafter | tabledeletecol tabledeleterow tabledelete";
7+
let toolbar = "undo redo | tablerowheader tablecolheader tablemergecells tablesplitcells tablecellprops tablerowprops | tableinsertcolbefore tableinsertcolafter tableinsertrowbefore tableinsertrowafter | tabledeletecol tabledeleterow tabledelete";
88
let contextmenu = "table";
9-
let valid_elements = "br,table[border|width|height|align|summary],tr[align|valign],td[align|valign|width|colspan|rowspan],th[align|valign|width|colspan|rowspan|scope],thead,tbody";
9+
let valid_elements = "br,table[border|width|height|align|summary|style],tr[align|valign|style],td[align|valign|width|colspan|rowspan|style],th[align|valign|width|colspan|rowspan|scope|style],thead,tbody";
10+
const valid_styles = {
11+
"th": "text-align,vertical-align,width",
12+
"td": "text-align,vertical-align,width",
13+
"tr": "text-align,width"
14+
}
15+
1016
if (this.meta.enableLinks) {
1117
plugins += " link autolink";
1218
toolbar += " | link";
@@ -27,6 +33,7 @@ class TinyTableBlockDefinition extends window.wagtailStreamField.blocks.FieldBlo
2733
toolbar: toolbar,
2834
contextmenu: contextmenu,
2935
valid_elements: valid_elements,
36+
valid_styles: valid_styles,
3037
table_toolbar: "", // disable the floating toolbar
3138
table_advtab: false,
3239
table_appearance_options: false,

src/wagtail_tinytableblock/utils.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import re
2+
13
from html import unescape
24
from typing import TYPE_CHECKING, Literal
35

@@ -16,8 +18,9 @@ def sanitise_html(content: str, *, allow_links: bool = False) -> str:
1618
tags: set[str] = {"table", "tr", "th", "td", "thead", "tbody", "caption"}
1719
attributes: dict[str, set[str]] = {
1820
"*": {"class"},
19-
"th": {"colspan", "rowspan", "align", "scope"},
20-
"td": {"colspan", "rowspan", "align", "scope"},
21+
"th": {"colspan", "rowspan", "align", "scope", "style"},
22+
"td": {"colspan", "rowspan", "align", "scope", "style"},
23+
"tr": {"style"},
2124
}
2225
if allow_links:
2326
tags |= {"a"}
@@ -31,6 +34,38 @@ def sanitise_html(content: str, *, allow_links: bool = False) -> str:
3134
)
3235

3336

37+
STYLE_PROPS_PATTERN = re.compile(r"([^\s:;]+)\s*:\s*([^;]+)")
38+
39+
40+
def clean_style_attributes(table: "Tag") -> None:
41+
"""
42+
TinyMCE sets align/width props in the style attribute,
43+
however nh3 yet support ammonia's filter_style_attributes.
44+
45+
TODO: remove once https://github.com/messense/nh3/issues/78 is fixed
46+
"""
47+
allowed_map = {
48+
"th": {"text-align", "vertical-align", "width"},
49+
"td": {"text-align", "vertical-align", "width"},
50+
"tr": {"text-align", "width"},
51+
}
52+
for tag in table.find_all(["tr", "th", "td"]):
53+
if "style" not in tag.attrs:
54+
continue
55+
56+
matches = STYLE_PROPS_PATTERN.findall(tag["style"])
57+
filtered_styles = []
58+
for prop, value in matches:
59+
prop = prop.strip()
60+
if prop in allowed_map[tag.name]:
61+
filtered_styles.append(f"{prop.strip()}: {value.strip()}")
62+
63+
if filtered_styles:
64+
tag["style"] = "; ".join(filtered_styles)
65+
else:
66+
del tag["style"]
67+
68+
3469
def get_cell_data(cell: "Tag", forced_type: Cell | None = None) -> dict[str, str | int]:
3570
value = "".join(str(child) for child in cell.children)
3671
cell_data = {"value": value, "type": forced_type or cell.name}
@@ -44,6 +79,15 @@ def get_cell_data(cell: "Tag", forced_type: Cell | None = None) -> dict[str, str
4479
if align := cell.get("align"):
4580
cell_data["align"] = align
4681

82+
if style := cell.get("style"):
83+
matches = dict(STYLE_PROPS_PATTERN.findall(style))
84+
if width := matches.get("width"):
85+
cell_data["width"] = width
86+
if align := matches.get("text-align"):
87+
cell_data["align"] = align
88+
if valign := matches.get("vertical-align"):
89+
cell_data["valign"] = valign
90+
4791
return cell_data
4892

4993

@@ -74,6 +118,8 @@ def html_table_to_dict(content: str, *, allow_links: bool = False) -> dict:
74118
"html": "",
75119
}
76120

121+
clean_style_attributes(table)
122+
77123
# Extract headers
78124
headers = []
79125
if thead := table.find("thead"):

tests/test_utils.py

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,63 @@ def test_table_with_thead_no_tbody(self):
167167
self.assertEqual(result["headers"][0][0]["value"], "Header 1")
168168
self.assertEqual(result["rows"][0][0]["value"], "Cell 1")
169169

170+
def test_table_with_style_attributes(self):
171+
"""Test parsing a simple table with thead and tbody."""
172+
html = """
173+
<table>
174+
<thead>
175+
<tr>
176+
<th style="width: 50px; text-align: left; vertical-align: top; color: red;">Header 1</th>
177+
<td align="right" style="width: 10%; text-align: center; display: none;">Header 2</td>
178+
</tr>
179+
</thead>
180+
<tbody>
181+
<tr>
182+
<td>Cell 1</td>
183+
<td>Cell 2</td>
184+
</tr>
185+
</tbody>
186+
</table>
187+
"""
188+
result = html_table_to_dict(html)
189+
190+
self.assertEqual(len(result["headers"]), 1)
191+
self.assertListEqual(
192+
result["headers"][0],
193+
[
194+
{
195+
"value": "Header 1",
196+
"type": "th",
197+
"align": "left",
198+
"valign": "top",
199+
"width": "50px",
200+
},
201+
{
202+
"value": "Header 2",
203+
"type": "th",
204+
"align": "center",
205+
"width": "10%",
206+
},
207+
],
208+
)
209+
210+
self.assertEqual(len(result["rows"]), 1)
211+
self.assertListEqual(
212+
result["rows"][0],
213+
[
214+
{
215+
"value": "Cell 1",
216+
"type": "td",
217+
},
218+
{
219+
"value": "Cell 2",
220+
"type": "td",
221+
},
222+
],
223+
)
224+
225+
self.assertEqual(result["html"], html)
226+
170227
def test_multiple_headers_structure(self):
171228
"""Test parsing a table with complex nested structure."""
172229
html = """
@@ -247,16 +304,16 @@ def test_sanitisation__with_links(self):
247304

248305
def test_sanitisation__no_links(self):
249306
html = """
250-
<table>
251-
<thead class="foo">
307+
<table style="width: 100%;">
308+
<thead class="foo" style="width: 100%;">
252309
<tr>
253-
<th data-test"foo" valign="middle" colspan="1">Header 1</th>
310+
<th data-test"foo" valign="middle" colspan="1" style="color: red;">Header 1</th>
254311
<th class="highlight" align="right">Header 2</th>
255312
</tr>
256313
</thead>
257-
<tbody>
314+
<tbody style="width: 100%;">
258315
<tr>
259-
<td rowspan="1">Cell <strong>1</strong></td>
316+
<td rowspan="1" style="text-align: right;">Cell <strong>1</strong></td>
260317
<td><a href="#" click='alert(\\'XSS\\')' rel="next">Cell 2</a></td>
261318
</tr>
262319
</tbody>
@@ -266,13 +323,13 @@ def test_sanitisation__no_links(self):
266323
<table>
267324
<thead class="foo">
268325
<tr>
269-
<th colspan="1">Header 1</th>
326+
<th colspan="1" style="color: red;">Header 1</th>
270327
<th class="highlight" align="right">Header 2</th>
271328
</tr>
272329
</thead>
273330
<tbody>
274331
<tr>
275-
<td rowspan="1">Cell 1</td>
332+
<td rowspan="1" style="text-align: right;">Cell 1</td>
276333
<td>Cell 2</td>
277334
</tr>
278335
</tbody>

0 commit comments

Comments
 (0)