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