"""
This module renders lookatme slides to HTML
"""
from collections import deque
from contextlib import contextmanager
import re
from typing import Dict, Optional
import urwid
import lookatme.config as config
from lookatme.widgets.clickable_text import LinkIndicatorSpec
import lookatme.utils as utils
[docs]class HtmlContext:
def __init__(self):
self.output = deque()
[docs] @contextmanager
def use_tag(
self,
tag_name: str,
classname: Optional[str] = None,
id: Optional[str] = None,
style: Optional[Dict[str, str]] = None,
**other_attrs,
):
attrs = other_attrs
if classname:
attrs["class"] = classname
if id:
attrs["id"] = id
if style:
attrs["style"] = "; ".join(f"{k}: {v}" for k, v in style.items())
attrs_str = ""
if attrs:
attrs_str = " ".join(f'{key}="{val}"' for key, val in attrs.items())
attrs_str = " " + attrs_str
self.output.append(f"<{tag_name}{attrs_str}>")
yield
self.output.append(f"</{tag_name}>")
[docs] @contextmanager
def use_spec(self, spec: Optional[urwid.AttrSpec], render_images: bool = True):
if spec is None:
yield
return
tag = "span"
extra_attrs = {}
styles = {}
swap_fg_bg_colors = False
for part in spec.foreground.split(","):
part = part.strip().lower()
if part.startswith("#"):
styles["color"] = part
elif part == "underline":
decoration = styles.setdefault("text-decoration", "")
styles["text-decoration"] = decoration + " underline"
elif part == "blink":
extra_attrs["classname"] = "blink"
elif part == "bold":
styles["font-weight"] = "bold"
elif part == "italics":
styles["font-style"] = "italic"
elif part == "standout":
swap_fg_bg_colors = True
elif part == "strikethrough":
decoration = styles.setdefault("text-decoration", "")
styles["text-decoration"] = decoration + " strikethrough"
if isinstance(spec, LinkIndicatorSpec) and render_images:
if spec.link_type == "link":
tag = "a"
extra_attrs["href"] = spec.link_target
extra_attrs["target"] = "blank"
elif spec.link_type == "image":
tag = "img"
extra_attrs["src"] = spec.link_target
if spec.background.startswith("#"):
styles["background-color"] = spec.background
if swap_fg_bg_colors:
fg = styles.get("color", "")
bg = styles.get("background-color", "")
styles["color"] = bg
styles["background-color"] = fg
with self.use_tag(tag, style=styles, **extra_attrs):
yield
[docs] def write(self, content: str):
self.output.append(content)
[docs] def get_html(self):
return "".join(self.output)
[docs] def get_html_consumed(self):
res = self.get_html()
self.output = []
return res
def _sanitize(text: str) -> str:
return text.replace("&", "&").replace("<", "<").replace(">", ">")
def _keep_text(text_idx: int, text: str, keep_range: range) -> str:
text_stop = text_idx + len(text)
if text_stop < keep_range.start:
return ""
if text_idx > keep_range.stop:
return ""
start = max(keep_range.start, text_idx)
stop = min(text_stop, keep_range.stop)
return text[start - text_idx : stop - text_idx]
def _space_span_replace(match: re.Match) -> str:
spaces = match.group(0)
return f"<span style='padding-left: {len(spaces)}ch'></span>"
[docs]def canvas_to_html(
ctx: HtmlContext,
canvas: urwid.Canvas,
only_keep: Optional[str] = None,
render_images: bool = True,
):
for idx, row in enumerate(canvas.content()):
only_keep_range = None
if only_keep:
start_idx = canvas.text[idx].decode().find(only_keep)
if start_idx == -1:
continue
only_keep_range = range(start_idx, start_idx + len(only_keep))
text_idx = 0
for spec, _, text in row:
text = text.decode()
if only_keep_range:
new_text = _keep_text(text_idx, text, only_keep_range)
text_idx += len(text)
text = new_text
text = _sanitize(text)
if getattr(spec, "preserve_spaces", False):
text = text.replace(" ", " ")
else:
text = re.sub(r"( {2,})", _space_span_replace, text)
if text == "":
continue
with ctx.use_spec(spec, render_images=render_images):
ctx.write(text)
if idx != len(canvas.text) - 1:
ctx.write("<br/>\n")
[docs]def add_styles_to_context(context: Dict):
styles = config.get_style()
flattened_styles = {}
utils.flatten_dict(styles, flattened_styles, ["styles"])
context.update(flattened_styles)