aboutsummaryrefslogtreecommitdiff
path: root/cros_utils/tiny_render.py
diff options
context:
space:
mode:
Diffstat (limited to 'cros_utils/tiny_render.py')
-rw-r--r--cros_utils/tiny_render.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/cros_utils/tiny_render.py b/cros_utils/tiny_render.py
new file mode 100644
index 00000000..629e7719
--- /dev/null
+++ b/cros_utils/tiny_render.py
@@ -0,0 +1,181 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""A super minimal module that allows rendering of readable text/html.
+
+Usage should be relatively straightforward. You wrap things you want to write
+out in some of the nice types defined here, and then pass the result to one of
+render_text_pieces/render_html_pieces.
+
+In HTML, the types should all nest nicely. In text, eh (nesting anything in
+Bold is going to be pretty ugly, probably).
+
+Lists and tuples may be used to group different renderable elements.
+
+Example:
+
+render_text_pieces([
+ Bold("Daily to-do list:"),
+ UnorderedList([
+ "Write code",
+ "Go get lunch",
+ ["Fix ", Bold("some"), " of the bugs in the aforementioned code"],
+ [
+ "Do one of the following:",
+ UnorderedList([
+ "Nap",
+ "Round 2 of lunch",
+ ["Look at ", Link("https://google.com/?q=memes", "memes")],
+ ]),
+ ],
+ "What a rough day; time to go home",
+ ]),
+])
+
+Turns into
+
+**Daily to-do list:**
+ - Write code
+ - Go get lunch
+ - Fix **some** of the bugs in said code
+ - Do one of the following:
+ - Nap
+ - Round 2 of lunch
+ - Look at memes
+ - What a rough day; time to go home
+
+...And similarly in HTML, though with an actual link.
+
+The rendering functions should never mutate your input.
+"""
+
+from __future__ import print_function
+
+import collections
+import html
+import typing as t
+
+Bold = collections.namedtuple('Bold', ['inner'])
+LineBreak = collections.namedtuple('LineBreak', [])
+Link = collections.namedtuple('Link', ['href', 'inner'])
+UnorderedList = collections.namedtuple('UnorderedList', ['items'])
+# Outputs different data depending on whether we're emitting text or HTML.
+Switch = collections.namedtuple('Switch', ['text', 'html'])
+
+line_break = LineBreak()
+
+# Note that these build up their values in a funky way: they append to a list
+# that ends up being fed to `''.join(into)`. This avoids quadratic string
+# concatenation behavior. Probably doesn't matter, but I care.
+
+# Pieces are really a recursive type:
+# Union[
+# Bold,
+# LineBreak,
+# Link,
+# List[Piece],
+# Tuple[...Piece],
+# UnorderedList,
+# str,
+# ]
+#
+# It doesn't seem possible to have recursive types, so just go with Any.
+Piece = t.Any # pylint: disable=invalid-name
+
+
+def _render_text_pieces(piece: Piece, indent_level: int,
+ into: t.List[str]) -> None:
+ """Helper for |render_text_pieces|. Accumulates strs into |into|."""
+ if isinstance(piece, LineBreak):
+ into.append('\n' + indent_level * ' ')
+ return
+
+ if isinstance(piece, str):
+ into.append(piece)
+ return
+
+ if isinstance(piece, Bold):
+ into.append('**')
+ _render_text_pieces(piece.inner, indent_level, into)
+ into.append('**')
+ return
+
+ if isinstance(piece, Link):
+ # Don't even try; it's ugly more often than not.
+ _render_text_pieces(piece.inner, indent_level, into)
+ return
+
+ if isinstance(piece, UnorderedList):
+ for p in piece.items:
+ _render_text_pieces([line_break, '- ', p], indent_level + 2, into)
+ return
+
+ if isinstance(piece, Switch):
+ _render_text_pieces(piece.text, indent_level, into)
+ return
+
+ if isinstance(piece, (list, tuple)):
+ for p in piece:
+ _render_text_pieces(p, indent_level, into)
+ return
+
+ raise ValueError('Unknown piece type: %s' % type(piece))
+
+
+def render_text_pieces(piece: Piece) -> str:
+ """Renders the given Pieces into text."""
+ into = []
+ _render_text_pieces(piece, 0, into)
+ return ''.join(into)
+
+
+def _render_html_pieces(piece: Piece, into: t.List[str]) -> None:
+ """Helper for |render_html_pieces|. Accumulates strs into |into|."""
+ if piece is line_break:
+ into.append('<br />\n')
+ return
+
+ if isinstance(piece, str):
+ into.append(html.escape(piece))
+ return
+
+ if isinstance(piece, Bold):
+ into.append('<b>')
+ _render_html_pieces(piece.inner, into)
+ into.append('</b>')
+ return
+
+ if isinstance(piece, Link):
+ into.append('<a href="' + piece.href + '">')
+ _render_html_pieces(piece.inner, into)
+ into.append('</a>')
+ return
+
+ if isinstance(piece, UnorderedList):
+ into.append('<ul>\n')
+ for p in piece.items:
+ into.append('<li>')
+ _render_html_pieces(p, into)
+ into.append('</li>\n')
+ into.append('</ul>\n')
+ return
+
+ if isinstance(piece, Switch):
+ _render_html_pieces(piece.html, into)
+ return
+
+ if isinstance(piece, (list, tuple)):
+ for p in piece:
+ _render_html_pieces(p, into)
+ return
+
+ raise ValueError('Unknown piece type: %s' % type(piece))
+
+
+def render_html_pieces(piece: Piece) -> str:
+ """Renders the given Pieces into HTML."""
+ into = []
+ _render_html_pieces(piece, into)
+ return ''.join(into)