1# -*- coding: utf-8 -*- 2# Copyright 2020 The Chromium OS Authors. All rights reserved. 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 54from __future__ import print_function 55 56import collections 57import html 58import typing as t 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(piece: Piece, indent_level: int, 89 into: t.List[str]) -> 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 = [] 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 = [] 180 _render_html_pieces(piece, into) 181 return ''.join(into) 182