#!/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 ''' ''' def tooltip_style(): '''Output enough CSS that we can bring up tooltips for table elements, describing what row and column they correspond to''' return ''' ''' 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 '
' % (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 = '' % (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 = '' % ( 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 = '' % ( 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 = '' % ( 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 = '' 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 '%s' % (Field.row_urls[self.row], self.string) return self.string else: if self.string in FALSE_VALUES: return '' return 'Yes' 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('\n' % self.id_table()) stuff.append('%s\n' % (INDENT, self.id_row())) stuff.append('%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('
') stuff.append(self.stringer()) if self.options.tooltips: stuff.append(self.span_end()) stuff.append('\n') if self.is_right(): stuff.append('%s\n' % INDENT) if self.is_bottom(): stuff.append('
') 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('' % 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('
\n' * 5) list_.append('
  • Rows
  • \n') list_.append('%s' % INDENT) return ''.join(list_) def column_selections(self): '''Print data for columns''' list_ = [] list_.append('
    \n' * 5) list_.append('
  • Columns
  • \n') list_.append('%s
    ' % INDENT) return ''.join(list_) def row_subtable(self): '''Print data for row subtables''' list_ = [] list_.append('
    \n' * 5) list_.append('
  • Row subtables
  • \n') list_.append('%s' % 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('
    \n' * 100) if options.subtables: print('\n') print(table.row_subtable()) print('
    \n' * 100) main()