# -*- coding: utf-8 -*- """Tablib - LaTeX table export support. Generates a LaTeX booktabs-style table from the dataset. """ import re from tablib.compat import unicode title = 'latex' extensions = ('tex',) TABLE_TEMPLATE = """\ %% Note: add \\usepackage{booktabs} to your preamble %% \\begin{table}[!htbp] \\centering %(CAPTION)s \\begin{tabular}{%(COLSPEC)s} \\toprule %(HEADER)s %(MIDRULE)s %(BODY)s \\bottomrule \\end{tabular} \\end{table} """ TEX_RESERVED_SYMBOLS_MAP = dict([ ('\\', '\\textbackslash{}'), ('{', '\\{'), ('}', '\\}'), ('$', '\\$'), ('&', '\\&'), ('#', '\\#'), ('^', '\\textasciicircum{}'), ('_', '\\_'), ('~', '\\textasciitilde{}'), ('%', '\\%'), ]) TEX_RESERVED_SYMBOLS_RE = re.compile( '(%s)' % '|'.join(map(re.escape, TEX_RESERVED_SYMBOLS_MAP.keys()))) def export_set(dataset): """Returns LaTeX representation of dataset :param dataset: dataset to serialize :type dataset: tablib.core.Dataset """ caption = '\\caption{%s}' % dataset.title if dataset.title else '%' colspec = _colspec(dataset.width) header = _serialize_row(dataset.headers) if dataset.headers else '' midrule = _midrule(dataset.width) body = '\n'.join([_serialize_row(row) for row in dataset]) return TABLE_TEMPLATE % dict(CAPTION=caption, COLSPEC=colspec, HEADER=header, MIDRULE=midrule, BODY=body) def _colspec(dataset_width): """Generates the column specification for the LaTeX `tabular` environment based on the dataset width. The first column is justified to the left, all further columns are aligned to the right. .. note:: This is only a heuristic and most probably has to be fine-tuned post export. Column alignment should depend on the data type, e.g., textual content should usually be aligned to the left while numeric content almost always should be aligned to the right. :param dataset_width: width of the dataset """ spec = 'l' for _ in range(1, dataset_width): spec += 'r' return spec def _midrule(dataset_width): """Generates the table `midrule`, which may be composed of several `cmidrules`. :param dataset_width: width of the dataset to serialize """ if not dataset_width or dataset_width == 1: return '\\midrule' return ' '.join([_cmidrule(colindex, dataset_width) for colindex in range(1, dataset_width + 1)]) def _cmidrule(colindex, dataset_width): """Generates the `cmidrule` for a single column with appropriate trimming based on the column position. :param colindex: Column index :param dataset_width: width of the dataset """ rule = '\\cmidrule(%s){%d-%d}' if colindex == 1: # Rule of first column is trimmed on the right return rule % ('r', colindex, colindex) if colindex == dataset_width: # Rule of last column is trimmed on the left return rule % ('l', colindex, colindex) # Inner columns are trimmed on the left and right return rule % ('lr', colindex, colindex) def _serialize_row(row): """Returns string representation of a single row. :param row: single dataset row """ new_row = [_escape_tex_reserved_symbols(unicode(item)) if item else '' for item in row] return 6 * ' ' + ' & '.join(new_row) + ' \\\\' def _escape_tex_reserved_symbols(input): """Escapes all TeX reserved symbols ('_', '~', etc.) in a string. :param input: String to escape """ def replace(match): return TEX_RESERVED_SYMBOLS_MAP[match.group()] return TEX_RESERVED_SYMBOLS_RE.sub(replace, input)