diff --git a/README.md b/README.md
index aa442709..bb365317 100644
--- a/README.md
+++ b/README.md
@@ -675,6 +675,16 @@ will print:
```
+#### Setting HTML escaping
+
+By default, PrettyTable will escape the data contained in the header and data fields
+when sending output to HTML. This can be disabled by setting the `escape_header` and
+`escape_data` to false. For example:
+
+```python
+print(table.get_html_string(escape_header=False, escape_data=False))
+```
+
### Miscellaneous things
#### Copying a table
diff --git a/src/prettytable/prettytable.py b/src/prettytable/prettytable.py
index 62a1df1d..ca8ae512 100644
--- a/src/prettytable/prettytable.py
+++ b/src/prettytable/prettytable.py
@@ -188,6 +188,8 @@ def __init__(self, field_names=None, **kwargs) -> None:
"max_width",
"min_width",
"none_format",
+ "escape_header",
+ "escape_data",
]
for option in self._options:
if option in kwargs:
@@ -224,6 +226,14 @@ def __init__(self, field_names=None, **kwargs) -> None:
self._reversesort = False
self._sort_key = kwargs["sort_key"] or (lambda x: x)
+ if kwargs["escape_data"] in (True, False):
+ self._escape_data = kwargs["escape_data"]
+ else:
+ self._escape_data = True
+ if kwargs["escape_header"] in (True, False):
+ self._escape_header = kwargs["escape_header"]
+ else:
+ self._escape_header = True
# Column specific arguments, use property.setters
self.align = kwargs["align"] or {}
self.valign = kwargs["valign"] or {}
@@ -380,6 +390,8 @@ def _validate_option(self, option, val) -> None:
"xhtml",
"print_empty",
"oldsortslice",
+ "escape_header",
+ "escape_data",
):
self._validate_true_or_false(option, val)
elif option == "header_style":
@@ -1252,6 +1264,26 @@ def oldsortslice(self, val) -> None:
self._validate_option("oldsortslice", val)
self._oldsortslice = val
+ @property
+ def escape_header(self):
+ """Escapes the text within a header (True or False)"""
+ return self._escape_header
+
+ @escape_header.setter
+ def escape_header(self, val):
+ self._validate_option("escape_header", val)
+ self._escape_header = val
+
+ @property
+ def escape_data(self):
+ """Escapes the text within a data field (True or False)"""
+ return self._escape_data
+
+ @escape_data.setter
+ def escape_data(self, val):
+ self._validate_option("escape_data", val)
+ self._escape_data = val
+
##############################
# OPTION MIXER #
##############################
@@ -2136,6 +2168,7 @@ def get_html_string(self, **kwargs) -> str:
end - index of last data row to include in output PLUS ONE (list slice style)
fields - names of fields (columns) to include
header - print a header showing field names (True or False)
+ escape_header - escapes the text within a header (True or False)
border - print a border around the table (True or False)
preserve_internal_border - print a border inside the table even if
border is disabled (True or False)
@@ -2156,6 +2189,7 @@ def get_html_string(self, **kwargs) -> str:
tag
format - Controls whether or not HTML tables are formatted to match
styling options (True or False)
+ escape_data - escapes the text within a data field (True or False)
xhtml - print
tags if True,
tags if False"""
options = self._get_options(kwargs)
@@ -2197,11 +2231,13 @@ def _get_simple_html_string(self, options):
for field in self._field_names:
if options["fields"] and field not in options["fields"]:
continue
+ if options["escape_header"]:
+ field = escape(field)
+
lines.append(
- " {} | ".format(
- escape(field).replace("\n", linebreak)
- )
+ " {} | ".format(field.replace("\n", linebreak))
)
+
lines.append(" ")
lines.append(" ")
@@ -2214,10 +2250,11 @@ def _get_simple_html_string(self, options):
for field, datum in zip(self._field_names, row):
if options["fields"] and field not in options["fields"]:
continue
+ if options["escape_data"]:
+ datum = escape(datum)
+
lines.append(
- " {} | ".format(
- escape(datum).replace("\n", linebreak)
- )
+ " {} | ".format(datum.replace("\n", linebreak))
)
lines.append(" ")
lines.append(" ")
@@ -2273,9 +2310,12 @@ def _get_formatted_html_string(self, options):
for field in self._field_names:
if options["fields"] and field not in options["fields"]:
continue
+ if options["escape_header"]:
+ field = escape(field)
+
lines.append(
' %s | ' # noqa: E501
- % (lpad, rpad, escape(field).replace("\n", linebreak))
+ % (lpad, rpad, field.replace("\n", linebreak))
)
lines.append(" ")
lines.append(" ")
@@ -2300,6 +2340,9 @@ def _get_formatted_html_string(self, options):
):
if options["fields"] and field not in options["fields"]:
continue
+ if options["escape_data"]:
+ datum = escape(datum)
+
lines.append(
' %s | ' # noqa: E501
% (
@@ -2307,7 +2350,7 @@ def _get_formatted_html_string(self, options):
rpad,
align,
valign,
- escape(datum).replace("\n", linebreak),
+ datum.replace("\n", linebreak),
)
)
lines.append(" ")
diff --git a/tests/test_prettytable.py b/tests/test_prettytable.py
index 9b51e4a2..750b84ce 100644
--- a/tests/test_prettytable.py
+++ b/tests/test_prettytable.py
@@ -1195,6 +1195,234 @@ def test_html_output_formatted_with_title(self) -> None:
+""".strip() # noqa: E501
+ )
+
+ def test_html_output_without_escaped_header(self) -> None:
+ t = helper_table(rows=0)
+ t.field_names = ["", "Field 1", "Field 2", "Field 3"]
+ result = t.get_html_string(escape_header=False)
+ assert (
+ result.strip()
+ == """
+
+
+
+ |
+ Field 1 |
+ Field 2 |
+ Field 3 |
+
+
+
+
+
+""".strip()
+ )
+
+ def test_html_output_without_escaped_data(self) -> None:
+ t = helper_table(rows=0)
+ t.add_row(
+ [
+ 1,
+ "value 1",
+ "value2",
+ "value3",
+ ]
+ )
+ result = t.get_html_string(escape_data=False)
+ assert (
+ result.strip()
+ == """
+
+
+
+ |
+ Field 1 |
+ Field 2 |
+ Field 3 |
+
+
+
+
+ 1 |
+ value 1 |
+ value2 |
+ value3 |
+
+
+
+""".strip()
+ )
+
+ def test_html_output_with_escaped_header(self) -> None:
+ t = helper_table(rows=0)
+ t.field_names = ["", "Field 1", "Field 2", "Field 3"]
+ result = t.get_html_string(escape_header=True)
+ assert (
+ result.strip()
+ == """
+
+
+
+ |
+ Field 1 |
+ <em>Field 2</em> |
+ <a href='#'>Field 3</a> |
+
+
+
+
+
+""".strip()
+ )
+
+ def test_html_output_with_escaped_data(self) -> None:
+ t = helper_table(rows=0)
+ t.add_row(
+ [
+ 1,
+ "value 1",
+ "value2",
+ "value3",
+ ]
+ )
+ result = t.get_html_string(escape_data=True)
+ assert (
+ result.strip()
+ == """
+
+
+
+ |
+ Field 1 |
+ Field 2 |
+ Field 3 |
+
+
+
+
+ 1 |
+ <b>value 1</b> |
+ <span style='text-decoration: underline;'>value2</span> |
+ <a href='#'>value3</a> |
+
+
+
+""".strip() # noqa: E501
+ )
+
+ def test_html_output_formatted_without_escaped_header(self) -> None:
+ t = helper_table(rows=0)
+ t.field_names = ["", "Field 1", "Field 2", "Field 3"]
+ result = t.get_html_string(escape_header=False, format=True)
+ assert (
+ result.strip()
+ == """
+
+
+
+ |
+ Field 1 |
+ Field 2 |
+ Field 3 |
+
+
+
+
+
+""".strip() # noqa: E501
+ )
+
+ def test_html_output_formatted_without_escaped_data(self) -> None:
+ t = helper_table(rows=0)
+ t.add_row(
+ [
+ 1,
+ "value 1",
+ "value2",
+ "value3",
+ ]
+ )
+ result = t.get_html_string(escape_data=False, format=True)
+ assert (
+ result.strip()
+ == """
+
+
+
+ |
+ Field 1 |
+ Field 2 |
+ Field 3 |
+
+
+
+
+ 1 |
+ value 1 |
+ value2 |
+ value3 |
+
+
+
+""".strip() # noqa: E501
+ )
+
+ def test_html_output_formatted_with_escaped_header(self) -> None:
+ t = helper_table(rows=0)
+ t.field_names = ["", "Field 1", "Field 2", "Field 3"]
+ result = t.get_html_string(escape_header=True, format=True)
+ assert (
+ result.strip()
+ == """
+
+
+
+ |
+ Field 1 |
+ <em>Field 2</em> |
+ <a href='#'>Field 3</a> |
+
+
+
+
+
+""".strip() # noqa: E501
+ )
+
+ def test_html_output_formatted_with_escaped_data(self) -> None:
+ t = helper_table(rows=0)
+ t.add_row(
+ [
+ 1,
+ "value 1",
+ "value2",
+ "value3",
+ ]
+ )
+ result = t.get_html_string(escape_data=True, format=True)
+ assert (
+ result.strip()
+ == """
+
+
+
+ |
+ Field 1 |
+ Field 2 |
+ Field 3 |
+
+
+
+
+ 1 |
+ <b>value 1</b> |
+ <span style='text-decoration: underline;'>value2</span> |
+ <a href='#'>value3</a> |
+
+
+
""".strip() # noqa: E501
)