1# -*- coding: utf-8 -*-
2# Copyright 2020 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""A super minimal module that allows rendering of readable text/html.
7
8Usage should be relatively straightforward. You wrap things you want to write
9out in some of the nice types defined here, and then pass the result to one of
10render_text_pieces/render_html_pieces.
11
12In HTML, the types should all nest nicely. In text, eh (nesting anything in
13Bold is going to be pretty ugly, probably).
14
15Lists and tuples may be used to group different renderable elements.
16
17Example:
18
19render_text_pieces([
20  Bold("Daily to-do list:"),
21  UnorderedList([
22    "Write code",
23    "Go get lunch",
24    ["Fix ", Bold("some"), " of the bugs in the aforementioned code"],
25    [
26      "Do one of the following:",
27      UnorderedList([
28        "Nap",
29        "Round 2 of lunch",
30        ["Look at ", Link("https://google.com/?q=memes", "memes")],
31      ]),
32    ],
33    "What a rough day; time to go home",
34  ]),
35])
36
37Turns into
38
39**Daily to-do list:**
40  - Write code
41  - Go get lunch
42  - Fix **some** of the bugs in said code
43  - Do one of the following:
44    - Nap
45    - Round 2 of lunch
46    - Look at memes
47  - What a rough day; time to go home
48
49...And similarly in HTML, though with an actual link.
50
51The rendering functions should never mutate your input.
52"""
53
54from __future__ import print_function
55
56import collections
57import html
58import typing as t
59
60Bold = collections.namedtuple('Bold', ['inner'])
61LineBreak = collections.namedtuple('LineBreak', [])
62Link = collections.namedtuple('Link', ['href', 'inner'])
63UnorderedList = collections.namedtuple('UnorderedList', ['items'])
64# Outputs different data depending on whether we're emitting text or HTML.
65Switch = collections.namedtuple('Switch', ['text', 'html'])
66
67line_break = LineBreak()
68
69# Note that these build up their values in a funky way: they append to a list
70# that ends up being fed to `''.join(into)`. This avoids quadratic string
71# concatenation behavior. Probably doesn't matter, but I care.
72
73# Pieces are really a recursive type:
74# Union[
75#   Bold,
76#   LineBreak,
77#   Link,
78#   List[Piece],
79#   Tuple[...Piece],
80#   UnorderedList,
81#   str,
82# ]
83#
84# It doesn't seem possible to have recursive types, so just go with Any.
85Piece = t.Any  # pylint: disable=invalid-name
86
87
88def _render_text_pieces(piece: Piece, indent_level: int,
89                        into: t.List[str]) -> None:
90  """Helper for |render_text_pieces|. Accumulates strs into |into|."""
91  if isinstance(piece, LineBreak):
92    into.append('\n' + indent_level * ' ')
93    return
94
95  if isinstance(piece, str):
96    into.append(piece)
97    return
98
99  if isinstance(piece, Bold):
100    into.append('**')
101    _render_text_pieces(piece.inner, indent_level, into)
102    into.append('**')
103    return
104
105  if isinstance(piece, Link):
106    # Don't even try; it's ugly more often than not.
107    _render_text_pieces(piece.inner, indent_level, into)
108    return
109
110  if isinstance(piece, UnorderedList):
111    for p in piece.items:
112      _render_text_pieces([line_break, '- ', p], indent_level + 2, into)
113    return
114
115  if isinstance(piece, Switch):
116    _render_text_pieces(piece.text, indent_level, into)
117    return
118
119  if isinstance(piece, (list, tuple)):
120    for p in piece:
121      _render_text_pieces(p, indent_level, into)
122    return
123
124  raise ValueError('Unknown piece type: %s' % type(piece))
125
126
127def render_text_pieces(piece: Piece) -> str:
128  """Renders the given Pieces into text."""
129  into = []
130  _render_text_pieces(piece, 0, into)
131  return ''.join(into)
132
133
134def _render_html_pieces(piece: Piece, into: t.List[str]) -> None:
135  """Helper for |render_html_pieces|. Accumulates strs into |into|."""
136  if piece is line_break:
137    into.append('<br />\n')
138    return
139
140  if isinstance(piece, str):
141    into.append(html.escape(piece))
142    return
143
144  if isinstance(piece, Bold):
145    into.append('<b>')
146    _render_html_pieces(piece.inner, into)
147    into.append('</b>')
148    return
149
150  if isinstance(piece, Link):
151    into.append('<a href="' + piece.href + '">')
152    _render_html_pieces(piece.inner, into)
153    into.append('</a>')
154    return
155
156  if isinstance(piece, UnorderedList):
157    into.append('<ul>\n')
158    for p in piece.items:
159      into.append('<li>')
160      _render_html_pieces(p, into)
161      into.append('</li>\n')
162    into.append('</ul>\n')
163    return
164
165  if isinstance(piece, Switch):
166    _render_html_pieces(piece.html, into)
167    return
168
169  if isinstance(piece, (list, tuple)):
170    for p in piece:
171      _render_html_pieces(p, into)
172    return
173
174  raise ValueError('Unknown piece type: %s' % type(piece))
175
176
177def render_html_pieces(piece: Piece) -> str:
178  """Renders the given Pieces into HTML."""
179  into = []
180  _render_html_pieces(piece, into)
181  return ''.join(into)
182