#!/usr/bin/python3 # pylint: disable=no-name-in-module,import-error,no-member,superfluous-parens # superfluous-parens: Parentheses are valuable for clarity and portability '''Create HTML tables from CSV inputs''' import sys try: import urllib.parse except ImportError: import urllib HAVE_URLLIB_PARSE = False else: HAVE_URLLIB_PARSE = True def usage(retval): '''Output a usage message and exit''' write = sys.stderr.write write("Convert a whitespace (or arbitrarily) delimited\n") write("CSV table, one row per line, to an HTML table.\n\n") write("Usage:\n") write(" -d use a delimiter of |\n") write(" --delimiter ch use a delimiter of ch\n") write(" --help this stuff\n") write(" --suppressible Output javascript code to allow suppressing unwanted rows and columns\n") write(" --tooltips Output CSS styling to allow row/column tooltips\n") write(' --selections Output clickable selections\n') write(" --yes-no Output yes's and no's (yes's and empty spaces really) instead of the\n") write(" actual table values\n") write(' --subtables Build a subtable for each row\n') write(' --links Hyperlink appropriately using 2nd column\n') write(' --no-comment Do not output the input CSV as a series of comments\n') sys.exit(retval) INDENT = ' ' FALSE_VALUES = frozenset(['', '0', 'n', 'no', 'No', 'N', 'false', 'False', 'n/a', 'N/A']) def supression_functions(): '''Output javascript code needed for suppressing rows and columns''' return ''' <script type="text/javascript"> function hide_row(row_name) { var row_element = document.getElementById(row_name); row_element.style.display = 'none'; } function hide_colgroup(column_name) { // Sadly, it appears that columns are not children of their tables - instead, rows are children of the table, // and portions of a given column is a child of multiple rows. var table_element = document.getElementById('table'); td_elements = table_element.getElementsByTagName('td'); for (td_elementno = 0; td_elementno < td_elements.length; td_elementno ++) { td_name = td_elements[td_elementno].getAttribute('name'); // Check if the td name is the ColumnID and change display if needed. if (td_name == column_name) { td_elements[td_elementno].style.display = 'none'; } } } </script> ''' def tooltip_style(): '''Output enough CSS that we can bring up tooltips for table elements, describing what row and column they correspond to''' return ''' <STYLE> .popup { COLOR: #8F040A; CURSOR: help; TEXT-DECORATION: none } </STYLE> ''' def deal_with_special_chars(human_readable_name): '''Escape special characters in a row or column name, for use in selecting a row or column by name''' spaces_dashed = human_readable_name.replace(' ', '-') if HAVE_URLLIB_PARSE: browser_happy_name = urllib.parse.quote(spaces_dashed) else: browser_happy_name = urllib.quote(spaces_dashed) return browser_happy_name class Field(object): '''Store one field in the table, and perform operations on it''' row_urls = [] def __init__(self, options, row, col, string): self.options = options self.row = row self.col = col self.string = string def is_true(self): '''Return true if this field has a value''' if self.string in FALSE_VALUES: return False return True def is_left(self): '''Is this field on the left edge of the table?''' return self.col == 0 def is_right(self): '''Is this field on the right edge of the table?''' return self.col == self.options.width - 1 def is_top(self): '''Is this field on the top edge of the table?''' return self.row == 0 def is_bottom(self): '''Is this field on the bottom edge of the table?''' return self.row == self.options.height - 1 def column_supression_button(self): '''Return the HTML for a column supression button''' if self.is_top() and not self.is_left(): return '<button onclick="hide_colgroup(\'col-%d\')">Hide column</button><br>' % (self.col + 1) return '' def row_supression_button(self): '''Return the HTML for a row supression button''' if not self.is_top() and self.is_left(): result = '<button onclick="hide_row(\'row-%d\')">Hide row</button>' % (self.row + 1) else: result = '' return result def column_selection_button(self): '''Return the HTML for a column selection button''' if self.is_top() and not self.is_left(): result = '<button onclick="window.open(\'#column-name-select-%s\')">Select column</button>' % ( deal_with_special_chars(self.string), ) else: result = '' return result def row_selection_button(self): '''Return the HTML for a row selection button''' if not self.is_top() and self.is_left(): result = '<button onclick="window.open(\'#row-name-select-%s\')">Select row</button>' % ( deal_with_special_chars(self.string), ) else: result = '' return result def id_row(self): '''Output a row id for supression''' if self.options.suppressible: result = ' id="row-%d"' % (self.row + 1) else: result = '' return result def id_col(self): '''Output a column id for supression''' if self.options.suppressible: result = ' name="col-%d"' % (self.col + 1) else: result = '' return result def id_table(self): '''Output a table id for supression''' if self.options.suppressible: result = 'id="table" ' else: result = '' return result def span_start(self, row_description, col_description): '''Output just the start of the CSS span''' if self.options.tooltips and not self.is_left() and not self.is_top(): result = '<span title="row: %s, column: %s " class="popup">' % ( row_description, col_description, ) else: result = '' return result def span_end(self): '''Output just the end of the CSS span''' if self.options.tooltips and not self.is_left() and not self.is_top(): result = '</span>' else: result = '' return result def stringer(self): '''Convert a field to a yes/no value, except for the left and top edges''' if self.is_left() or self.is_top() or not self.options.yesno: if self.is_left() and not self.is_top() and self.options.links: return '<a href="%s">%s</a>' % (Field.row_urls[self.row], self.string) return self.string else: if self.string in FALSE_VALUES: return '' return '<font color="green">Yes</font>' def to_string(self, row_description, col_description): '''Convert this field to a string, with appropriate HTML/CSS/Javascript added''' stuff = [] if self.is_left(): if self.is_top(): if self.options.tooltips: stuff.append(tooltip_style()) if self.options.suppressible: stuff.append(supression_functions()) stuff.append('<table %swidth="100%%" border="1">\n' % self.id_table()) stuff.append('%s<tr%s>\n' % (INDENT, self.id_row())) stuff.append('%s<td%s>' % (INDENT*2, self.id_col())) if self.options.suppressible: stuff.append(self.column_supression_button()) stuff.append(self.row_supression_button()) if self.options.selections: stuff.append(self.column_selection_button()) stuff.append(self.row_selection_button()) if self.options.tooltips: stuff.append(self.span_start(row_description, col_description)) if self.is_top(): stuff.append('<br>') stuff.append(self.stringer()) if self.options.tooltips: stuff.append(self.span_end()) stuff.append('</td>\n') if self.is_right(): stuff.append('%s</tr>\n' % INDENT) if self.is_bottom(): stuff.append('</table>') return ''.join(stuff) def read_table_as_strings(options): '''Read our table as a series of strings from stdin''' rows = [] row_urls = [] # Read the table from stdin, into a list of lists containing strings, sans padding - that comes later unprocessed_table = [] for line in sys.stdin: line = line.rstrip('\n') line = line.replace('""', '"') unprocessed_table.append(line) if options.delimiter: field_strings = line.split(options.delimiter) else: field_strings = line.split() if options.links: # We're skipping the second - it's a URL that we don't want to display. Instead we hyperlink to it modified_rows = [field_strings[0]] + field_strings[2:] rows.append(modified_rows) row_urls.append(field_strings[1]) else: rows.append(field_strings) if options.comment: for unprocessed_line in unprocessed_table: greater_line = unprocessed_line.replace('>', '>') print('<!-- %s -->' % greater_line) # Compute the height of the table height = len(rows) # Compute the maximum width of all rows width = 0 for row in rows: width = max(width, len(row)) # Pad out short rows with empty fields for row in rows: while len(row) < width: row.append('') Field.row_urls = row_urls return width, height, rows class Table(object): # pylint: disable=R0903 # R0903: We don't need a lot of public methods '''Store all the fields of a table and perform operations on that table''' def __init__(self, options): self.options = options del options self.options.width, self.options.height, table_as_strings = read_table_as_strings(self.options) # create a list of lists of Fields from the list of lists of strings self.rows = [] for row in range(self.options.height): self.rows.append([]) for col in range(self.options.width): field = Field(self.options, row, col, table_as_strings[row][col]) self.rows[-1].append(field) def get_field(self, row, col): '''Look up one field by row and column''' return self.rows[row][col] def __str__(self): '''Output one cell in the table''' list_of_str = [] for row in range(self.options.height): row_field = self.get_field(row, 0) row_string = row_field.string for col in range(self.options.width): col_field = self.get_field(0, col) col_string = col_field.string field = self.get_field(row, col) list_of_str.append(field.to_string(row_string, col_string)) return ''.join(list_of_str) def row_selections(self): '''Print data for rows''' list_ = [] list_.append('<br>\n' * 5) list_.append('<li>Rows</li>\n') list_.append('%s<ul>\n' % INDENT) for row in range(1, self.options.height): list_.append('%s<a name="row-select-%d">\n' % (INDENT, row + 1)) list_.append('%s<a name="row-name-select-%s">\n' % (INDENT, deal_with_special_chars(self.get_field(row, 0).string))) string = self.get_field(row, 0).stringer() list_.append('%s<li>%s</li>\n' % (INDENT, string)) list_.append('%s<ol>\n' % (INDENT*2)) for col in range(1, self.options.width): field = self.get_field(row, col) if field.is_true(): list_.append('%s<li>%s</li>\n' % (INDENT*2, self.get_field(0, col).stringer())) list_.append('%s</ol>\n\n' % (INDENT*2)) list_.append('%s</ul>' % INDENT) return ''.join(list_) def column_selections(self): '''Print data for columns''' list_ = [] list_.append('<br>\n' * 5) list_.append('<li>Columns</li>\n') list_.append('%s<ul>\n' % INDENT) for col in range(1, self.options.width): list_.append('%s<a name="column-select-%d">\n' % (INDENT, col + 1)) list_.append('%s<a name="column-name-select-%s">\n' % (INDENT, deal_with_special_chars(self.get_field(0, col).string))) string = self.get_field(0, col).stringer() list_.append('%s<li>%s</li>\n' % (INDENT, string)) list_.append('%s<ol>\n' % (INDENT*2)) for row in range(1, self.options.height): field = self.get_field(row, col) if field.is_true(): list_.append('%s<li>%s</li>\n' % (INDENT*2, self.get_field(row, 0).stringer())) list_.append('%s</ol>\n\n' % (INDENT*2)) list_.append('%s</ul>' % INDENT) return ''.join(list_) def row_subtable(self): '''Print data for row subtables''' list_ = [] list_.append('<br>\n' * 5) list_.append('<li>Row subtables</li>\n') list_.append('%s<ul>\n' % INDENT) for row in range(1, self.options.height): list_.append('%s<li>%s</li>\n' % (INDENT, self.get_field(row, 0).stringer())) list_.append('%s<table width="100%%" border="1">\n' % (INDENT*2)) for col in range(1, self.options.width): list_.append('%s<tr>\n' % (INDENT*3)) list_.append( '%s<td>%s</td> <td>%s</td>\n' % (INDENT*4, self.get_field(0, col).string, self.get_field(row, col).string), ) list_.append('%s</tr>\n' % (INDENT*3)) list_.append('%s</table>\n\n' % (INDENT*2)) list_.append('%s</ul>' % INDENT) return ''.join(list_) class Options(object): # pylint: disable=R0903,R0902 # R0903: We don't need a lot of public methods # R0902: We need a bunch of instance attributes because we're mostly a container '''Hold options related to command line arguments, plus the width and height of a table''' def __init__(self): self.delimiter = None self.suppressible = False self.tooltips = False self.selections = False self.width = None self.height = None self.yesno = False self.subtables = False self.links = False self.comment = True while sys.argv[1:]: if sys.argv[1] == '-d': self.delimiter = '|' elif sys.argv[1] == '--delimiter' and sys.argv[2:]: self.delimiter = sys.argv[2] del sys.argv[2] elif sys.argv[1] in ['-h', '--help']: usage(0) elif sys.argv[1] == '--suppressible': self.suppressible = True elif sys.argv[1] == '--tooltips': self.tooltips = True elif sys.argv[1] == '--selections': self.selections = True elif sys.argv[1] == '--yes-no': self.yesno = True elif sys.argv[1] == '--subtables': self.subtables = True elif sys.argv[1] == '--links': self.links = True elif sys.argv[1] == '--no-comment': self.comment = False else: sys.stderr.write('%s: Unrecognized option: %s\n' % (sys.argv[0], sys.argv[1])) usage(1) del sys.argv[1] def main(): '''Main function''' options = Options() table = Table(options) print(str(table)) if options.selections: print('\n') print(table.row_selections()) print('\n') print(table.column_selections()) print('<br>\n' * 100) if options.subtables: print('\n') print(table.row_subtable()) print('<br>\n' * 100) main()