xref: /aosp_15_r20/external/toolchain-utils/cros_utils/tiny_render.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1# Copyright 2020 The ChromiumOS Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""A super minimal module that allows rendering of readable text/html.
6
7Usage should be relatively straightforward. You wrap things you want to write
8out in some of the nice types defined here, and then pass the result to one of
9render_text_pieces/render_html_pieces.
10
11In HTML, the types should all nest nicely. In text, eh (nesting anything in
12Bold is going to be pretty ugly, probably).
13
14Lists and tuples may be used to group different renderable elements.
15
16Example:
17
18render_text_pieces([
19  Bold("Daily to-do list:"),
20  UnorderedList([
21    "Write code",
22    "Go get lunch",
23    ["Fix ", Bold("some"), " of the bugs in the aforementioned code"],
24    [
25      "Do one of the following:",
26      UnorderedList([
27        "Nap",
28        "Round 2 of lunch",
29        ["Look at ", Link("https://google.com/?q=memes", "memes")],
30      ]),
31    ],
32    "What a rough day; time to go home",
33  ]),
34])
35
36Turns into
37
38**Daily to-do list:**
39  - Write code
40  - Go get lunch
41  - Fix **some** of the bugs in said code
42  - Do one of the following:
43    - Nap
44    - Round 2 of lunch
45    - Look at memes
46  - What a rough day; time to go home
47
48...And similarly in HTML, though with an actual link.
49
50The rendering functions should never mutate your input.
51"""
52
53
54import collections
55import html
56import typing as t
57
58
59Bold = collections.namedtuple("Bold", ["inner"])
60LineBreak = collections.namedtuple("LineBreak", [])
61Link = collections.namedtuple("Link", ["href", "inner"])
62UnorderedList = collections.namedtuple("UnorderedList", ["items"])
63# Outputs different data depending on whether we're emitting text or HTML.
64Switch = collections.namedtuple("Switch", ["text", "html"])
65
66line_break = LineBreak()
67
68# Note that these build up their values in a funky way: they append to a list
69# that ends up being fed to `''.join(into)`. This avoids quadratic string
70# concatenation behavior. Probably doesn't matter, but I care.
71
72# Pieces are really a recursive type:
73# Union[
74#   Bold,
75#   LineBreak,
76#   Link,
77#   List[Piece],
78#   Tuple[...Piece],
79#   UnorderedList,
80#   str,
81# ]
82#
83# It doesn't seem possible to have recursive types, so just go with Any.
84Piece = t.Any  # pylint: disable=invalid-name
85
86
87def _render_text_pieces(
88    piece: Piece, indent_level: int, into: t.List[str]
89) -> 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: t.List[str] = []
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: t.List[str] = []
180    _render_html_pieces(piece, into)
181    return "".join(into)
182