Source code for docria.printout

# -*- coding: utf-8 -*-
#
# Copyright 2021 Marcus Klang (marcus.klang@cs.lth.se)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Presentation module, utilities for formatting document objects."""

from typing import List, Dict, Tuple, Optional
from html import escape
import re


[docs]class PrintOptions: """ Presentation settings :note: Setting any setting to None will disable truncation for that aspect. """
[docs] def __init__(self): """The maximum number of nodes to output, -1 for infinite""" self._max_rows = 100 self._max_columns = 8 self._max_column_width = 30 self._max_display_width = 120
@property def max_rows(self): """Max number of rows, will truncate table if larger.""" return self._max_rows @max_rows.setter def max_rows(self, max_rows): if max_rows is None: self._max_rows = None elif max_rows <= 0: raise ValueError("Max rows must be None for infinite or >= 1") else: self._max_rows = int(max_rows) @property def max_columns(self): """Max number of columns to display""" return self._max_rows @max_columns.setter def max_columns(self, max_columns): if max_columns is None: self._max_columns = None elif max_columns <= 0: raise ValueError("Max columns must be None for infinite or >= 1") else: self._max_columns = int(max_columns) @property def max_column_width(self): """Max column width, the maximum number of characters to show inside a column, will truncate if larger.""" return self._max_column_width @max_column_width.setter def max_column_width(self, max_column_width): if max_column_width is None: self._max_column_width = None elif max_column_width <= 6: raise ValueError("Max column width must be None for infinite or >= 7") else: self._max_column_width = int(max_column_width) @property def max_display_width(self): """Max display width, the maximum number of characters in width for a full table, columns will be wrapped if longer.""" return self._max_display_width @max_display_width.setter def max_display_width(self, max_display_width): if max_display_width is None: self._max_display_width = None elif max_display_width <= 10: raise ValueError("Max column width must be None for infinite or >= 10") else: self._max_display_width = int(max_display_width)
options = PrintOptions()
[docs]def set_large_screen(): """Sets options to higher than default widths""" global options options.max_column_width = 200 options.max_display_width = 200
class TableStyle: def __init__(self, padding=2): self.padding = padding def truncate(text): if options.max_column_width is None: return text else: min_sz = min(options._max_display_width-5, options.max_column_width-5) if len(text) > min_sz: part = min_sz >> 1 return "%s ... %s" % (text[0:part], text[-part:]) else: return text class TableRow: def __init__(self, *elems, index=None): """ :param elems: The content of the row :param index: The index name of this row, is used by table to change id to a named index """ self.index = index self.elems = list(elems) def num_columns(self): return len(self.elems) class TableCell: def __init__(self, text=None, html=None): self.text = text self.html = html URN_MATCH = re.compile(r"urn:(\w+):(.+)") def urn_link_wikidata(partial): return "https://www.wikidata.org/wiki/{0}".format(escape(partial)) def urn_link_wikipedia(partial): parts = partial.split(":", 1) return "https://{0}.wikipedia.org/wiki/{1}".format(escape(parts[0]), escape(parts[1])) URN_LINK_FN = { "wikidata": urn_link_wikidata, # ex: urn:wikidata:Q34 - Sweden "wikipedia": urn_link_wikipedia # ex: urn:wikipedia:sv:Sverige - Sverige }
[docs]class Table: """Table representation for text and HTML"""
[docs] def __init__(self, caption: Optional[str]=None, style=TableStyle(), hide_index=False, hide_headers=False): self.caption = caption self.style = style # type: TableStyle self.header = None # type: TableRow self.body = [] # type: List[TableRow] self.hide_header = hide_headers self.hide_index = hide_index
def set_header(self, *row): if len(row) == 1 and isinstance(row[0], TableRow): self.header = row[0] row[0].index = "#" else: self.header = TableRow(*row, index="#") def add_body(self, *row): if len(row) == 1 and isinstance(row[0], TableRow): self.body.append(row[0]) else: self.body.append(TableRow(*row)) def set_footer(self, *row): if len(row) == 1 and isinstance(row[0], TableRow): self.footer = row[0] else: self.footer = TableRow(*row) def format_text(self, row: TableRow, index: int): output = [str(row.index) if row.index is not None else str(index)] for col in row.elems: if isinstance(col, TableCell): output.append(truncate(col.text)) else: output.append(truncate(str(col))) return output def format_html(self, row: TableRow, index: int): output = [str(row.index) if row.index is not None else str(index)] for col in row.elems: if isinstance(col, TableCell): output.append(col.html) continue elif isinstance(col, str): if URN_MATCH.fullmatch(col) is not None: parts = col.split(":", 2) urn_type = parts[1].lower() if urn_type in URN_LINK_FN: output.append("<a href='{0}'>{1}</a>".format( URN_LINK_FN[urn_type](parts[2]), escape(truncate(str(col))) ) ) continue output.append(escape(truncate(str(col)))) return output def _compile_text(self): headers = self.format_text(self.header, 0) if options.max_rows is not None and len(self.body) > options.max_rows: body = [] part = options.max_rows >> 1 upper_part = zip(range(0, part), self.body[0:part]) lower_part = zip(range(len(self.body)-part, len(self.body)), self.body[-part:]) body.extend(list(map(lambda tup: self.format_text(row=tup[1], index=tup[0]), upper_part))) body.append(None) body.extend(list(map(lambda tup: self.format_text(row=tup[1], index=tup[0]), lower_part))) else: body = list(map(lambda tup: self.format_text(row=tup[1], index=tup[0]), zip(range(len(self.body)), self.body))) return headers, body def _compile_html(self): headers = self.format_html(self.header, 0) if options.max_rows is not None and len(self.body) > options.max_rows: body = [] part = options.max_rows >> 1 upper_part = zip(range(0, part), self.body[0:part]) lower_part = zip(range(len(self.body)-part, len(self.body)), self.body[-part:]) body.extend(list(map(lambda tup: self.format_html(row=tup[1], index=tup[0]), upper_part))) body.append(None) body.extend(list(map(lambda tup: self.format_html(row=tup[1], index=tup[0]), lower_part))) else: body = list(map(lambda tup: self.format_html(tup[1], tup[0]), zip(range(len(self.body)), self.body))) return headers, body def _get_column_format(self, width): return "%s%s%s" % (" " * self.style.padding, "{:<%d}" % width, " " * self.style.padding) def render_text(self): output = [] headers, rows = self._compile_text() col_widths = list(map(len, headers)) row_widths = [max(map(len, map(lambda row: row[i], filter(lambda x: x is not None, rows))), default=0) for i in range(len(headers))] if self.hide_header: actual_widths = row_widths else: actual_widths = list(map(max, zip(col_widths, row_widths))) column_pos = 1 while column_pos < len(headers): column_width = 0 if not self.hide_index: column_width += actual_widths[0] + self.style.padding*2 for i in range(column_pos, len(headers)): column_width += actual_widths[i] + self.style.padding*2 column_end = i+1 if options.max_display_width is not None and column_width > options.max_display_width: break if column_pos == 1 and self.caption is not None: # Print caption output.append(str.format("{:^%d}" % column_width, truncate(self.caption))) output.append("") elif column_pos > 1: output.append("") output.append("") # Print header if self.hide_index: real_header = headers[column_pos:column_end] line_format = "".join([self._get_column_format(actual_widths[k]) for k in range(column_pos, column_end)]) else: real_header = [headers[0]] + headers[column_pos:column_end] line_format = "".join([self._get_column_format(actual_widths[0])] + [self._get_column_format(actual_widths[k]) for k in range(column_pos, column_end)]) if not self.hide_header: output.append(str.format(line_format, *real_header) + (" \\" if column_end != len(headers) else "")) output.append("") # Print body if self.hide_index: for row in rows: if row is None: output.append("") output.append(str.format("{:^%d}" % column_width, "...")) output.append("") else: output.append(str.format(line_format, *row[column_pos:column_end])) else: for row in rows: if row is None: output.append("") output.append(str.format("{:^%d}" % column_width, "...")) output.append("") else: real_row = [row[0]] real_row.extend(row[column_pos:column_end]) output.append(str.format(line_format, *real_row)) column_pos = column_end output.append("") return "\n".join(output) def render_html(self): output = ["<table>", "<thead>"] headers, rows = self._compile_html() if self.caption is not None: output.append("<tr>") output.append("<th colspan='{0}' style='text-align:center'><em>".format(len(headers))) output.append(escape(self.caption)) output.append("</em></th></tr>") output.append("<tr>") for header in headers: output.append("<th style='text-align:left'>") output.append(header) output.append("</th>") output.append("</tr>") output.append("</thead>") output.append("<tbody>") for row in rows: if row is None: output.append("<tr><td colspan='{0}' style='text-align:center'>. . .</td></tr>".format(len(headers))) else: output.append("<tr>") for col in row: output.append("<td style='text-align:left'>{0}</td>".format(col)) output.append("</tr>") output.append("</tbody>") output.append("</table>") return "".join(output)
_NIL_VALUE = TableCell(text="NIL", html="<em>NIL</em>") def get_representation(value): from docria.model import Node if value is None: return _NIL_VALUE elif isinstance(value, list): if len(value) == 1: return "[%s]" % repr(value[0]) elif len(value) > 0: all_nodes = True nodetypes = set() for elem in value: if isinstance(elem, Node): nodetypes.add(elem.collection.name) else: all_nodes = False break if all_nodes: return "[%d nodes from layer: %s]" % (len(value), ", ".join(nodetypes)) else: return "[%d nodes from %d layers]" % (len(value), len(nodetypes)) else: return "[]" else: return repr(value)