# --------------------------------------------------------- # 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 = '<table style="width:100%">{0}</table>' ROW_FMT = "<tr>{0}</tr>" HEADER_FMT = "<th>{0}</th>" DATA_FMT = "<td>{0}</td>" # target="_blank" opens in new tab, rel="noopener" is for perf + security LINK_FMT = '<a href="{0}" target="_blank" rel="noopener">{1}</a>' 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 = ["""<table style="width:100%; border:2px solid black" >"""] if header is not None: html_row = "</td><td>".join(column for column in header) html.append( """<tr style="font-weight:bold; border-bottom:1pt solid black; border-right: 1pt solid black; text-align: center"><td>{}</td></tr>""".format( html_row ) ) for row in rows: html_row = "</td><td>".join(str(value) for value in row) html.append( """<tr style="width:100%; word-wrap: break-word; border-bottom:1pt solid black; text-align: center"><td>{}</td></tr>""".format( html_row ) ) html.append("</table>") return "".join(html)