You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
182 lines
4.7 KiB
182 lines
4.7 KiB
# -*- 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)
|