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