• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2# Copyright 2020 The ChromiumOS Authors
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
54
55import collections
56import html
57import typing as t
58
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(
89    piece: Piece, indent_level: int, into: t.List[str]
90) -> None:
91    """Helper for |render_text_pieces|. Accumulates strs into |into|."""
92    if isinstance(piece, LineBreak):
93        into.append("\n" + indent_level * " ")
94        return
95
96    if isinstance(piece, str):
97        into.append(piece)
98        return
99
100    if isinstance(piece, Bold):
101        into.append("**")
102        _render_text_pieces(piece.inner, indent_level, into)
103        into.append("**")
104        return
105
106    if isinstance(piece, Link):
107        # Don't even try; it's ugly more often than not.
108        _render_text_pieces(piece.inner, indent_level, into)
109        return
110
111    if isinstance(piece, UnorderedList):
112        for p in piece.items:
113            _render_text_pieces([line_break, "- ", p], indent_level + 2, into)
114        return
115
116    if isinstance(piece, Switch):
117        _render_text_pieces(piece.text, indent_level, into)
118        return
119
120    if isinstance(piece, (list, tuple)):
121        for p in piece:
122            _render_text_pieces(p, indent_level, into)
123        return
124
125    raise ValueError("Unknown piece type: %s" % type(piece))
126
127
128def render_text_pieces(piece: Piece) -> str:
129    """Renders the given Pieces into text."""
130    into = []
131    _render_text_pieces(piece, 0, into)
132    return "".join(into)
133
134
135def _render_html_pieces(piece: Piece, into: t.List[str]) -> None:
136    """Helper for |render_html_pieces|. Accumulates strs into |into|."""
137    if piece is line_break:
138        into.append("<br />\n")
139        return
140
141    if isinstance(piece, str):
142        into.append(html.escape(piece))
143        return
144
145    if isinstance(piece, Bold):
146        into.append("<b>")
147        _render_html_pieces(piece.inner, into)
148        into.append("</b>")
149        return
150
151    if isinstance(piece, Link):
152        into.append('<a href="' + piece.href + '">')
153        _render_html_pieces(piece.inner, into)
154        into.append("</a>")
155        return
156
157    if isinstance(piece, UnorderedList):
158        into.append("<ul>\n")
159        for p in piece.items:
160            into.append("<li>")
161            _render_html_pieces(p, into)
162            into.append("</li>\n")
163        into.append("</ul>\n")
164        return
165
166    if isinstance(piece, Switch):
167        _render_html_pieces(piece.html, into)
168        return
169
170    if isinstance(piece, (list, tuple)):
171        for p in piece:
172            _render_html_pieces(p, into)
173        return
174
175    raise ValueError("Unknown piece type: %s" % type(piece))
176
177
178def render_html_pieces(piece: Piece) -> str:
179    """Renders the given Pieces into HTML."""
180    into = []
181    _render_html_pieces(piece, into)
182    return "".join(into)
183