# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
import logging
from collections import OrderedDict
from datetime import datetime, timedelta
from html import escape
SUPPORTED_VALUE_TYPE_TUPLE = (int, float, str, datetime, timedelta)
TABLE_FMT = '
'
ROW_FMT = "{0}
"
HEADER_FMT = "{0} | "
DATA_FMT = "{0} | "
# target="_blank" opens in new tab, rel="noopener" is for perf + security
LINK_FMT = '{1}'
def convert_dict_to_table(object_to_convert):
# Case 1: All non-collection values -> table of 1 row
# Case 2: All collection values, lens eq N -> assert lengths eq, table of N rows
# Case 3: collection and non-collection values -> table of 1 row, collections nest
# Case 4: All collection values, lens unequal -> table of 1 row, each is nested collection?
if not isinstance(object_to_convert, dict):
raise AssertionError("Expected a dict or subclass, got {0}".format(type(object_to_convert)))
if len(object_to_convert) == 0:
return ""
ordered_obj = OrderedDict(object_to_convert)
def is_collection_type(val):
return hasattr(val, "__len__") and not isinstance(val, str)
is_collection = [is_collection_type(value) for value in ordered_obj.values()]
all_rows = [get_header_row_string(ordered_obj.keys())]
def values_to_data_row(values):
cells = [to_html(value) for value in values]
return get_data_row_string(cells)
if all(is_collection):
# Cases 2 and 4
length = len(list(ordered_obj.values)[0])
if any(len(v) != length for v in ordered_obj.values()):
# Case 4
logging.warning("Uneven column lengths in table conversion")
all_rows.append(values_to_data_row(ordered_obj.values()))
else:
# Case 2 - sad transpose
for i in range(length):
value_list = [val[i] for val in ordered_obj.values()]
all_rows.append(values_to_data_row(value_list))
else:
# Cases 1 and 3
# Table of 1 row of values or mixed types, table of 1 row
all_rows.append(values_to_data_row(ordered_obj.values()))
return table_from_html_rows(all_rows)
def convert_list_to_table(object_to_convert):
if not isinstance(object_to_convert, list):
raise AssertionError("Expected a list or subclass, got {0}".format(type(object_to_convert)))
if len(object_to_convert) == 0:
return ""
all_values = [to_html(element) for element in object_to_convert]
all_rows = [get_data_row_string([element]) for element in all_values]
return table_from_html_rows(all_rows)
# Mapping from complex type to HTML converters
# Unspecified types default to convert_value
_type_to_converter = {list: convert_list_to_table, dict: convert_dict_to_table}
def to_html(object_to_convert):
candidate_converters = [k for k in _type_to_converter if isinstance(object_to_convert, k)]
if len(candidate_converters) == 0:
converter = convert_value
elif len(candidate_converters) == 1:
converter = _type_to_converter[candidate_converters[0]]
else:
logging.warning("Multiple candidate converters found for type %s", type(object_to_convert))
converter = convert_value
converted_value = converter(object_to_convert)
return converted_value
def is_string_link(string: str) -> bool:
return isinstance(string, str) and string.strip().lower().startswith("http")
def make_link(link_string: str, link_text: str = "") -> str:
if not link_text: # Actually want truthy string
link_text = "Link"
return LINK_FMT.format(escape(link_string), link_text)
def convert_value(value: str) -> str:
if value is None:
return ""
if is_string_link(value):
return make_link(value)
if not isinstance(value, SUPPORTED_VALUE_TYPE_TUPLE):
logging.warning("Unsupported type %s for html, converting", type(value))
# TODO: Figure out a good escaping story here right now it breaks existing tags
return str(value)
def get_header_row_string(column_headers):
headers = [HEADER_FMT.format(header) for header in column_headers]
return ROW_FMT.format("".join(headers))
def get_data_row_string(data_values):
data = [DATA_FMT.format(datum) for datum in data_values]
return ROW_FMT.format("".join(data))
def table_from_html_rows(list_of_rows):
# type/: (List[str]) -> str
return TABLE_FMT.format("".join(list_of_rows))
def to_formatted_html_table(rows, header):
html = [""""""]
if header is not None:
html_row = "".join(column for column in header)
html.append(
""" | {} |
""".format(
html_row
)
)
for row in rows:
html_row = "".join(str(value) for value in row)
html.append(
""" | {} |
""".format(
html_row
)
)
html.append("
")
return "".join(html)