docs: def _parse_sheet(workbook, sheet):
      """
      The universal spreadsheet parser. Parse chron or paleo tables of type ensemble/model/summary.
      :param str name: Filename
      :param obj workbook: Excel Workbook
      :param dict sheet: Sheet path and naming info
      :return dict dict: Table metadata and numeric data
      """
      logger_excel.info("enter parse_sheet: {}".format(sheet["old_name"]))
  
      # Markers to track where we are on the sheet
      ensemble_on = False
      var_header_done = False
      metadata_on = False
      metadata_done = False
      data_on = False
      notes = False
  
      # Open the sheet from the workbook
      temp_sheet = workbook.sheet_by_name(sheet["old_name"])
      filename = sheet["filename"]
  
      # Store table metadata and numeric data separately
      table_name = "{}DataTableName".format(sheet["paleo_chron"])
  
      # Organize our root table data
      table_metadata = OrderedDict()
      table_metadata[table_name] = sheet["new_name"]
      table_metadata['filename'] = filename
      table_metadata['missingValue'] = 'nan'
      if "ensemble" in sheet["new_name"]:
          ensemble_on = True
  
      # Store all CSV in here by rows
      table_data = {filename: []}
  
      # Master list of all column metadata
      column_metadata = []
  
      # Index tracks which cells are being parsed
      num_col = 0
      num_row = 0
      nrows = temp_sheet.nrows
      col_total = 0
  
      # Tracks which "number" each metadata column is assigned
      col_add_ct = 1
  
      header_keys = []
      variable_keys = []
      variable_keys_lower = []
      mv = ""
  
      try:
          # Loop for every row in the sheet
          for i in range(0, nrows):
              # Hold the contents of the current cell
              cell = temp_sheet.cell_value(num_row, num_col)
              row = temp_sheet.row(num_row)
  
              # Skip all template lines
              if isinstance(cell, str):
                  # Note and missing value entries are rogue. They are not close to the other data entries.
                  if cell.lower().strip() not in EXCEL_TEMPLATE:
  
                      if "notes" in cell.lower() and not metadata_on:
                          # Store at the root table level
                          nt = temp_sheet.cell_value(num_row, 1)
                          if nt not in EXCEL_TEMPLATE:
                              table_metadata["notes"] = nt
  
                      elif cell.lower().strip() in ALTS_MV:
                          # Store at the root table level and in our function
                          mv = temp_sheet.cell_value(num_row, 1)
                          # Add if not placeholder value
                          if mv not in EXCEL_TEMPLATE:
                              table_metadata["missingValue"] = mv
  
                      # Variable template header row
                      elif cell.lower() in EXCEL_HEADER and not metadata_on and not data_on:
  
                          # Grab the header line
                          row = temp_sheet.row(num_row)
                          header_keys = _get_header_keys(row)
  
                          # Turn on the marker
                          var_header_done = True
  
                      # Data section (bottom of sheet)
                      elif data_on:
  
                          # Parse the row, clean, and add to table_data
                          table_data = _parse_sheet_data_row(temp_sheet, num_row, col_total, table_data, filename, mv)
  
                      # Metadata section. (top)
                      elif metadata_on:
  
                          # Reached an empty cell while parsing metadata. Mark the end of the section.
                          if cell in EMPTY:
                              metadata_on = False
                              metadata_done = True
  
                              # Create a list of all the variable names found
                              for entry in column_metadata:
                                  try:
                                      # var keys is used as the variableName entry in each column's metadata
                                      variable_keys.append(entry["variableName"].strip())
                                      # var keys lower is used for comparing and finding the data header row
                                      variable_keys_lower.append(entry["variableName"].lower().strip())
                                  except KeyError:
                                      # missing a variableName key
                                      pass
  
                          # Not at the end of the section yet. Parse the metadata
                          else:
                              # Get the row data
                              row = temp_sheet.row(num_row)
  
                              # Get column metadata
                              col_tmp = _compile_column_metadata(row, header_keys, col_add_ct)
  
                              # Append to master list
                              column_metadata.append(col_tmp)
                              col_add_ct += 1
  
                      # Variable metadata, if variable header exists
                      elif var_header_done and not metadata_done:
  
                          # Start piecing column metadata together with their respective variable keys
                          metadata_on = True
  
                          # Get the row data
                          row = temp_sheet.row(num_row)
  
                          # Get column metadata
                          col_tmp = _compile_column_metadata(row, header_keys, col_add_ct)
  
                          # Append to master list
                          column_metadata.append(col_tmp)
                          col_add_ct += 1
  
                      # Variable metadata, if variable header does not exist
                      elif not var_header_done and not metadata_done and cell:
                          # LiPD Version 1.1 and earlier: Chronology sheets don't have variable headers
                          # We could blindly parse, but without a header row_num we wouldn't know where
                          # to save the metadata
                          # Play it safe and assume data for first column only: variable name
                          metadata_on = True
  
                          # Get the row data
                          row = temp_sheet.row(num_row)
  
                          # Get column metadata
                          col_tmp = _compile_column_metadata(row, header_keys, col_add_ct)
  
                          # Append to master list
                          column_metadata.append(col_tmp)
                          col_add_ct += 1
  
                      # Data variable header row. Column metadata exists and metadata_done marker is on.
                      # This is where we compare top section variableNames to bottom section variableNames to see if
                      # we need to start parsing the column values
                      else:
                          try:
                              # Clean up variable_keys_lower so we all variable names change from "age(yrs BP)" to "age"
                              # Units in parenthesis make it too difficult to compare variables. Remove them.
                              row = _rm_units_from_var_names_multi(row)
  
                              if metadata_done and any(i in row for i in variable_keys_lower):
                                  data_on = True
  
                                  # Take the difference of the two lists. If anything exists, then that's a problem
                                  __compare_vars(row, variable_keys_lower, sheet["old_name"])
  
                                  # Ensemble columns are counted differently.
                                  if ensemble_on:
                                      # Get the next row, and count the data cells.
                                      col_total = len(temp_sheet.row(num_row+1))
                                      # If there's an empty row, between, then try the next row.
                                      if col_total < 2:
                                          col_total = temp_sheet.row(num_row + 2)
                                      try:
                                          ens_cols = []
                                          [ens_cols.append(i+1) for i in range(0, col_total-1)]
                                          column_metadata[1]["number"] = ens_cols
                                      except IndexError:
                                          logger_excel.debug("excel: parse_sheet: unable to add ensemble 'number' key")
                                      except KeyError:
                                          logger_excel.debug("excel: parse_sheet: unable to add ensemble 'number' list at key")
  
                                  # All other cass, columns are the length of column_metadata
                                  else:
                                      col_total = len(column_metadata)
  
                          except AttributeError:
                              pass
                              # cell is not a string, and lower() was not a valid call.
  
              # If this is a numeric cell, 99% chance it's parsing the data columns.
              elif isinstance(cell, float) or isinstance(cell, int):
                  if data_on or metadata_done:
  
                      # Parse the row, clean, and add to table_data
                      table_data = _parse_sheet_data_row(temp_sheet, num_row, col_total, table_data, filename, mv)
  
              # Move on to the next row
              num_row += 1
          table_metadata["columns"] = column_metadata
      except IndexError as e:
          logger_excel.debug("parse_sheet: IndexError: sheet: {}, row_num: {}, col_num: {}, {}".format(sheet, num_row, num_col, e))
  
      # If there isn't any data in this sheet, and nothing was parsed, don't let this
      # move forward to final output.
      if not table_data[filename]:
          table_data = None
          table_metadata = None
  
      logger_excel.info("exit parse_sheet")
      return table_metadata, table_data