aboutsummaryrefslogtreecommitdiff
path: root/cros_utils/tiny_render.py
blob: 629e7719353acdf4a9185535cd0639c4c8e17e87 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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)