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