from typing import List, Dict, Optional, cast, Tuple
import csscompressor
from xlsx2html.constants.border import DEFAULT_BORDER_STYLE, BORDER_STYLES
from xlsx2html.parser.cell import CellInfo, Border, Borders
from xlsx2html.parser.image import ImageInfo
from xlsx2html.parser.parser import Column, ParserResult
from xlsx2html.utils import hash_str
from xlsx2html.utils.render import render_attrs, render_inline_styles
from xlsx2html.utils.style import compress_style, StyleType, BorderType
[docs]class HtmlRenderer:
def __init__(
self,
display_grid: bool = False,
default_border_style: Optional[BorderType] = None,
table_attrs: Optional[StyleType] = None,
inline_styles: bool = False,
):
self.default_border_style = default_border_style or {}
self.display_grid = display_grid
self.table_attrs = table_attrs or {}
self.inline_styles = inline_styles
[docs] def render(self, result: ParserResult) -> str:
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
%s
</body>
</html>
"""
self.build_style_cache(result.rows)
h = [self.render_table(result)]
if not self.inline_styles:
css_tag = f'<style type="text/css">{self.render_css() or ""}</style>'
h.append(css_tag)
return html % "\n".join(h)
[docs] def render_table(self, result: ParserResult, attrs: Optional[StyleType] = None) -> str:
t_attrs: StyleType = dict(border="0", cellspacing="0", cellpadding="0")
t_attrs.update(self.table_attrs)
t_attrs.update(attrs or {})
h = [
"<table " 'style="border-collapse: collapse" ' f"{render_attrs(t_attrs)}" ">",
self.render_columns(result.cols),
]
if self.display_grid:
h.append(self.render_header(result.cols))
for row in result.rows:
trow = ["<tr>"]
for i, cell in enumerate(row):
if i == 0 and self.display_grid:
trow.append(self.render_lineno(cell.row))
images = result.images.get((cell.column, cell.row)) or []
trow.append(self.render_cell(cell, images))
trow.append("</tr>")
h.append("\n".join(trow))
h.append("</table>")
return "\n".join(h)
[docs] def render_lineno(self, lineno: int) -> str:
return f"<th>{lineno}</th>"
[docs] def render_columns(self, cols: List[Column]) -> str:
h = ["<colgroup>"]
for c in cols:
h.append(self.render_column(c))
h.append("</colgroup>")
return "\n".join(h)
[docs] def render_column(self, col: Column) -> str:
return f'<col style="width: {col.width}px"/>'
[docs] def render_cell(
self, cell: CellInfo, images: List[ImageInfo], attrs: Optional[StyleType] = None
) -> str:
formatted_images = "\n".join([self.render_image(img) for img in images])
c_attrs = {"id": cell.id, "colspan": cell.colspan, "rowspan": cell.rowspan}
class_name = self._cell_style_map[cell.coordinate]
if not self.inline_styles:
c_attrs["class"] = class_name
# Diagonal border
# TODO implement
# extra_classes = self.get_diagonal_border_style(cell.border)
# if extra_classes:
# for k, v in extra_classes.items():
# class_name, style_str = self._hash_style(v)
# self._style_hash_map[class_name + k] = style_str
# c_attrs["class"] += " " + class_name
else:
c_attrs["style"] = self._style_hash_map[class_name]
c_attrs.update(attrs or {})
return ("<td {attrs_str}>" "{formatted_images}" "{formatted_value}" "</td>").format(
attrs_str=render_attrs(c_attrs),
formatted_images=formatted_images,
formatted_value=cell.formatted_value,
)
[docs] def get_border_style_from_cell(self, cell: CellInfo) -> StyleType:
style: StyleType = {}
if not cell.border:
return style
def _get_border_style(b: Border, prefix: str = "border") -> StyleType:
border_style: StyleType = cast(StyleType, BORDER_STYLES.get(b.style) or {})
_style: StyleType = {}
if not border_style and b.style:
border_style = cast(StyleType, DEFAULT_BORDER_STYLE)
elif not b.style:
if isinstance(self.default_border_style, str):
# Maybe shortland
return {prefix: self.default_border_style}
border_style = self.default_border_style
for k, v in border_style.items():
_style[f"{prefix}-{k}"] = v
if b.color:
_style[f"{prefix}-color"] = b.color
return _style
if isinstance(cell.border, Border):
style.update(_get_border_style(cell.border))
else:
for b_dir in ["right", "left", "top", "bottom"]:
b_s = getattr(cell.border, b_dir)
if not b_s:
continue
style.update(_get_border_style(b_s, f"border-{b_dir}"))
return style
[docs] def get_diagonal_border_style(self, border: Borders) -> Dict:
class_styles = {}
base_style = {
"content": "' '",
"width": "100%",
"height": "100%",
"display": "block",
"position": "absolute",
"background-color": "red",
"top": "0",
"left": "0",
}
if border.diagonal_up:
class_styles["::after"] = {
**base_style,
"content": "'up'",
"transform": "rotate(-45deg)",
}
if border.diagonal_down:
class_styles["::before"] = {
**base_style,
"content": "'down'",
"border-color": "white transparent",
}
return class_styles
[docs] def get_styles_from_cell(
self, cell: CellInfo, extra_style: Optional[StyleType] = None
) -> StyleType:
h_styles: StyleType = {"height": f"{cell.height}pt"}
h_styles.update(self.get_border_style_from_cell(cell))
h_styles.update(extra_style or {})
h_styles["text-align"] = cell.alignment.horizontal
h_styles["vertical-align"] = cell.alignment.vertical
if cell.alignment.indent:
h_styles["text-indent"] = f"{cell.alignment.indent * 10}pt"
if cell.alignment.text_rotation:
h_styles["transform"] = f"rotate({cell.alignment.text_rotation}deg)"
if cell.fill and cell.fill.pattern == "solid":
# TODO patternType != 'solid'
h_styles["background-color"] = cell.fill.color
if cell.font:
text_decoration: List[str] = []
h_styles["font-size"] = "%spx" % cell.font.size
if cell.font.color:
h_styles["color"] = cell.font.color
if cell.font.bold:
h_styles["font-weight"] = "bold"
if cell.font.italic:
h_styles["font-style"] = "italic"
if cell.font.underline:
text_decoration.append("underline")
if cell.font.strike:
text_decoration.append("line-through")
if cell.font.overline:
text_decoration.append("overline")
if cell.font.outline:
f_color = cell.font.color or "#000"
h_styles["color"] = "white"
h_styles["text-shadow"] = (
f"-1px -1px 0 {f_color},"
f"1px -1px 0 {f_color},"
f"-1px 1px 0 {f_color},"
f"1px 1px 0 {f_color}"
)
if cell.font.shadow:
t_shadow = h_styles.get("text-shadow") or ""
if t_shadow:
t_shadow += ", "
o = "1px"
if cell.font.outline:
o = "2px"
t_shadow += f"{o} {o} 0 #333"
h_styles["text-shadow"] = t_shadow
h_styles["text-decoration"] = " ".join(text_decoration)
return h_styles
[docs] def render_image(self, image: ImageInfo) -> str:
styles = render_inline_styles(
{
"margin-left": f"{image.offset.x}px",
"margin-top": f"{image.offset.y}px",
"position": "absolute",
}
)
return (
f'<img width="{image.width}" height="{image.height}"'
f'style="{styles}"'
f'src="{image.src}"'
"/>"
)
def _hash_style(self, style: StyleType, prefix: str = "td-") -> Tuple[str, str]:
style_str = render_inline_styles(style)
class_name = prefix + hash_str(style_str)
return class_name, style_str
[docs] def build_style_cache(self, rows: List[List[CellInfo]]) -> None:
cell_style_map: Dict[str, str] = {}
style_hash_map: Dict[str, str] = {}
for row in rows:
for cell in row:
style = self.get_styles_from_cell(cell)
style = compress_style(style)
class_name, style_str = self._hash_style(style)
style_hash_map[class_name] = style_str
cell_style_map[cell.coordinate] = class_name
self._cell_style_map = cell_style_map
self._style_hash_map = style_hash_map
[docs] def render_css(self) -> str:
if self.inline_styles:
return ""
css = []
for c_name, style in self._style_hash_map.items():
css.append(f"td.{c_name} {{ {style} }}")
css_str = "\n".join(css)
# TODO add compress css
css_str = csscompressor.compress(css=css_str)
return css_str