blob: 629e7719353acdf4a9185535cd0639c4c8e17e87 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2020 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A super minimal module that allows rendering of readable text/html.
Usage should be relatively straightforward. You wrap things you want to write
out in some of the nice types defined here, and then pass the result to one of
render_text_pieces/render_html_pieces.
In HTML, the types should all nest nicely. In text, eh (nesting anything in
Bold is going to be pretty ugly, probably).
Lists and tuples may be used to group different renderable elements.
Example:
render_text_pieces([
Bold("Daily to-do list:"),
UnorderedList([
"Write code",
"Go get lunch",
["Fix ", Bold("some"), " of the bugs in the aforementioned code"],
[
"Do one of the following:",
UnorderedList([
"Nap",
"Round 2 of lunch",
["Look at ", Link("https://google.com/?q=memes", "memes")],
]),
],
"What a rough day; time to go home",
]),
])
Turns into
**Daily to-do list:**
- Write code
- Go get lunch
- Fix **some** of the bugs in said code
- Do one of the following:
- Nap
- Round 2 of lunch
- Look at memes
- What a rough day; time to go home
...And similarly in HTML, though with an actual link.
The rendering functions should never mutate your input.
"""
from __future__ import print_function
import collections
import html
import typing as t
Bold = collections.namedtuple('Bold', ['inner'])
LineBreak = collections.namedtuple('LineBreak', [])
Link = collections.namedtuple('Link', ['href', 'inner'])
UnorderedList = collections.namedtuple('UnorderedList', ['items'])
# Outputs different data depending on whether we're emitting text or HTML.
Switch = collections.namedtuple('Switch', ['text', 'html'])
line_break = LineBreak()
# Note that these build up their values in a funky way: they append to a list
# that ends up being fed to `''.join(into)`. This avoids quadratic string
# concatenation behavior. Probably doesn't matter, but I care.
# Pieces are really a recursive type:
# Union[
# Bold,
# LineBreak,
# Link,
# List[Piece],
# Tuple[...Piece],
# UnorderedList,
# str,
# ]
#
# It doesn't seem possible to have recursive types, so just go with Any.
Piece = t.Any # pylint: disable=invalid-name
def _render_text_pieces(piece: Piece, indent_level: int,
into: t.List[str]) -> None:
"""Helper for |render_text_pieces|. Accumulates strs into |into|."""
if isinstance(piece, LineBreak):
into.append('\n' + indent_level * ' ')
return
if isinstance(piece, str):
into.append(piece)
return
if isinstance(piece, Bold):
into.append('**')
_render_text_pieces(piece.inner, indent_level, into)
into.append('**')
return
if isinstance(piece, Link):
# Don't even try; it's ugly more often than not.
_render_text_pieces(piece.inner, indent_level, into)
return
if isinstance(piece, UnorderedList):
for p in piece.items:
_render_text_pieces([line_break, '- ', p], indent_level + 2, into)
return
if isinstance(piece, Switch):
_render_text_pieces(piece.text, indent_level, into)
return
if isinstance(piece, (list, tuple)):
for p in piece:
_render_text_pieces(p, indent_level, into)
return
raise ValueError('Unknown piece type: %s' % type(piece))
def render_text_pieces(piece: Piece) -> str:
"""Renders the given Pieces into text."""
into = []
_render_text_pieces(piece, 0, into)
return ''.join(into)
def _render_html_pieces(piece: Piece, into: t.List[str]) -> None:
"""Helper for |render_html_pieces|. Accumulates strs into |into|."""
if piece is line_break:
into.append('<br />\n')
return
if isinstance(piece, str):
into.append(html.escape(piece))
return
if isinstance(piece, Bold):
into.append('<b>')
_render_html_pieces(piece.inner, into)
into.append('</b>')
return
if isinstance(piece, Link):
into.append('<a href="' + piece.href + '">')
_render_html_pieces(piece.inner, into)
into.append('</a>')
return
if isinstance(piece, UnorderedList):
into.append('<ul>\n')
for p in piece.items:
into.append('<li>')
_render_html_pieces(p, into)
into.append('</li>\n')
into.append('</ul>\n')
return
if isinstance(piece, Switch):
_render_html_pieces(piece.html, into)
return
if isinstance(piece, (list, tuple)):
for p in piece:
_render_html_pieces(p, into)
return
raise ValueError('Unknown piece type: %s' % type(piece))
def render_html_pieces(piece: Piece) -> str:
"""Renders the given Pieces into HTML."""
into = []
_render_html_pieces(piece, into)
return ''.join(into)