Source code for lookatme.render.markdown_block

"""
Defines render functions that render lexed markdown block tokens into urwid
representations
"""


import mistune
import pygments
import pygments.styles
import urwid

import lookatme.config as config
import lookatme.render.markdown_inline as markdown_inline_renderer
import lookatme.render.pygments as pygments_render
import lookatme.utils as utils
from lookatme.contrib import contrib_first
from lookatme.tutorial import tutor
from lookatme.widgets.clickable_text import ClickableText


def _meta(item):
    if not hasattr(item, "meta"):
        meta = {}
        setattr(item, "meta", meta)
    else:
        meta = getattr(item, "meta")
    return meta


def _set_is_list(item, level=1, ordered=False):
    _meta(item).update({
        "is_list": True,
        "list_level": level,
        "ordered": ordered,
        "item_count": 0,
    })


def _inc_item_count(item):
    _meta(item)["item_count"] += 1
    return _meta(item)["item_count"]


def _is_list(item):
    return _meta(item).get("is_list", False)


def _list_level(item):
    return _meta(item).get("list_level", 1)


[docs]@contrib_first def render_newline(token, body, stack, loop): """Render a newline See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. """ return urwid.Divider()
[docs]@contrib_first def render_hrule(token, body, stack, loop): """Render a newline See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. """ hrule_conf = config.get_style()["hrule"] div = urwid.Divider(hrule_conf['char'], top=1, bottom=1) return urwid.Pile([urwid.AttrMap(div, utils.spec_from_style(hrule_conf['style']))])
[docs]@tutor( "markdown", "headings", r""" Headings are specified by prefixing text with `#` characters: <TUTOR:EXAMPLE> ## Heading Level 2 ### Heading Level 3 #### Heading Level 4 ##### Heading Level 5 </TUTOR:EXAMPLE> ## Style Headings can be styled with slide metadata. This is the default style: <TUTOR:STYLE>headings</TUTOR:STYLE> """, ) @contrib_first def render_heading(token, body, stack, loop): """Render markdown headings, using the defined styles for the styling and prefix/suffix. See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. Below are the default stylings for headings: .. code-block:: yaml headings: '1': bg: default fg: '#9fc,bold' prefix: "██ " suffix: "" '2': bg: default fg: '#1cc,bold' prefix: "▓▓▓ " suffix: "" '3': bg: default fg: '#29c,bold' prefix: "▒▒▒▒ " suffix: "" '4': bg: default fg: '#66a,bold' prefix: "░░░░░ " suffix: "" default: bg: default fg: '#579,bold' prefix: "░░░░░ " suffix: "" :returns: A list of urwid Widgets or a single urwid Widget """ headings = config.get_style()["headings"] level = token["level"] style = config.get_style()["headings"].get(str(level), headings["default"]) prefix = utils.styled_text(style["prefix"], style) suffix = utils.styled_text(style["suffix"], style) rendered = render_text(text=token["text"]) if len(rendered) > 0: rendered = rendered[0] return [ urwid.Divider(), ClickableText( [prefix] + utils.styled_text(rendered, style) + [suffix]), # type: ignore urwid.Divider(), ]
[docs]@tutor( "markdown", "tables", r""" Rows in tables are defined by separating columns with `|` characters. The header row is the first row defined and is separated by hypens (`---`). Alignment within a column can be set by adding a colon, `:`, to the left, right, or both ends of a header's separator. <TUTOR:EXAMPLE> | left align | centered | right align | |------------|:--------:|------------:| | 1 | a | A | | 11 | aa | AA | | 111 | aaa | AAA | | 1111 | aaaaa | AAAA | </TUTOR:EXAMPLE> ## Style Tables can be styled with slide metadata. This is the default style: <TUTOR:STYLE>table</TUTOR:STYLE> """, ) @contrib_first def render_table(token, body, stack, loop): """Renders a table using the :any:`Table` widget. See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. The table widget makes use of the styles below: .. code-block:: yaml table: column_spacing: 3 header_divider: "─" :returns: A list of urwid Widgets or a single urwid Widget """ from lookatme.widgets.table import Table headers = token["header"] aligns = token["align"] cells = token["cells"] table = Table(cells, headers=headers, aligns=aligns) padding = urwid.Padding(table, width=table.total_width + 2, align="center") def table_changed(*args, **kwargs): padding.width = table.total_width + 2 urwid.connect_signal(table, "change", table_changed) return padding
[docs]@contrib_first def render_list_start(token, body, stack, loop): """Handles the indentation when starting rendering a new list. List items themselves (with the bullets) are rendered by the :any:`render_list_item_start` function. See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. """ res = urwid.Pile([]) in_list = _is_list(stack[-1]) list_level = 1 if in_list: list_level = _list_level(stack[-1]) + 1 _set_is_list(res, list_level, ordered=token['ordered']) _meta(res)['list_start_token'] = token _meta(res)['max_list_marker_width'] = token.get('max_list_marker_width', 2) stack.append(res) widgets = [] if not in_list: widgets.append(urwid.Divider()) widgets.append(urwid.Padding(res, left=2)) widgets.append(urwid.Divider()) return widgets return res
[docs]@contrib_first def render_list_end(token, body, stack, loop): """Pops the pushed ``urwid.Pile()`` from the stack (decreases indentation) See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. """ meta = _meta(stack[-1]) meta['list_start_token']['max_list_marker_width'] = meta['max_list_marker_width'] stack.pop()
@tutor( "markdown", "ordered lists", r""" Ordered lists are lines of text prefixed by a `N. ` or `N)`, where `N` is any number. <TUTOR:EXAMPLE> 1. item 1. item 1. item 5. item 6. item 1. item 1. item </TUTOR:EXAMPLE> ## Style Ordered lists can be styled with slide metadata. This is the default style: <TUTOR:STYLE>numbering</TUTOR:STYLE> """ ) @tutor( "markdown", "unordered lists", r""" Unordered lists are lines of text starting with either `*`, `+`, or `-`. <TUTOR:EXAMPLE> * item * item * item * item * item * item * item </TUTOR:EXAMPLE> ## Style Unordered lists can be styled with slide metadata. This is the default style: <TUTOR:STYLE>bullets</TUTOR:STYLE> """ ) @tutor( "markdown", "lists", r""" Lists can either be ordered or unordered. You can nest lists by indenting child lists by four spaces. Other markdown elements can also be nested in lists. <TUTOR:EXAMPLE> 1. item > quote 1. item * item 1. item a paragraph More text here blah blah blah 1. A new item * item ```python print("hello") ``` 1. item </TUTOR:EXAMPLE> """ ) def _list_item_start(token, body, stack, loop): """Render the start of a list item. This function makes use of two different styles, one each for unordered lists (bullet styles) and ordered lists (numbering styles): .. code-block:: yaml bullets: '1': "•" '2': "⁃" '3': "◦" default: "•" numbering: '1': "numeric" '2': "alpha" '3': "roman" default: "numeric" See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. """ list_level = _list_level(stack[-1]) curr_count = _inc_item_count(stack[-1]) pile = urwid.Pile(urwid.SimpleFocusListWalker([])) meta = _meta(stack[-1]) if meta["ordered"]: numbering = config.get_style()["numbering"] list_marker_type = numbering.get(str(list_level), numbering["default"]) sequence = { "numeric": lambda x: str(x), "alpha": lambda x: chr(ord("a") + x - 1), "roman": lambda x: utils.int_to_roman(x), }[list_marker_type] list_marker = sequence(curr_count) + "." else: bullets = config.get_style()["bullets"] list_marker = bullets.get(str(list_level), bullets["default"]) marker_text = list_marker + " " if len(marker_text) > meta["max_list_marker_width"]: meta["max_list_marker_width"] = len(marker_text) marker_col_width = meta["max_list_marker_width"] res = urwid.Text(("bold", marker_text)) res = urwid.Columns([ (marker_col_width, urwid.Text(("bold", marker_text))), pile, ]) stack.append(pile) return res
[docs]@contrib_first def render_list_item_start(token, body, stack, loop): """Render the start of a list item. This function makes use of the styles: .. code-block:: yaml bullets: '1': "•" '2': "⁃" '3': "◦" default: "•" See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. """ return _list_item_start(token, body, stack, loop)
[docs]@contrib_first def render_loose_item_start(token, body, stack, loop): """Render the start of a list item. This function makes use of the styles: .. code-block:: yaml bullets: '1': "•" '2': "⁃" '3': "◦" default: "•" See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. """ return _list_item_start(token, body, stack, loop)
[docs]@contrib_first def render_list_item_end(token, body, stack, loop): """Pops the pushed ``urwid.Pile()`` from the stack (decreases indentation) See :any:`lookatme.tui.SlideRenderer.do_render` for argument and return value descriptions. """ stack.pop()
[docs]@contrib_first def render_text(token=None, body=None, stack=None, loop=None, text=None): """Renders raw text. This function uses the inline markdown lexer from mistune with the :py:mod:`lookatme.render.markdown_inline` render module to render the lexed inline markup to a list composed of widgets or `urwid Text markup <http://urwid.org/manual/displayattributes.html#text-markup>`_. The created list of widgets/Text markup is then used to create and return a list composed entirely of widgets and :any:`ClickableText` instances. Many other functions call this function directly, passing in the extra ``text`` argument and leaving all other arguments blank. See :any:`lookatme.tui.SlideRenderer.do_render` for additional argument and return value descriptions. """ if text is None and token is not None: text = token["text"] inline_lexer = mistune.InlineLexer(markdown_inline_renderer) res = inline_lexer.output(text) if len(res) == 0: res = [""] widget_list = [] curr_text_spec = [] for item in res: if isinstance(item, urwid.Widget): if len(curr_text_spec) > 0: widget_list.append(ClickableText(curr_text_spec)) curr_text_spec = [] widget_list.append(item) else: curr_text_spec.append(item) if len(curr_text_spec) > 0: widget_list.append(ClickableText(curr_text_spec)) return widget_list
[docs]@tutor( "markdown", "paragraph", r""" Paragraphs in markdown are simply text with a full empty line between them: <TUTOR:EXAMPLE> paragraph 1 paragraph 2 </TUTOR:EXAMPLE> ## Style Paragraphs cannot be styled in lookatme. """, ) @contrib_first def render_paragraph(token, body, stack, loop): """Renders the provided text with additional pre and post paddings. See :any:`lookatme.tui.SlideRenderer.do_render` for additional argument and return value descriptions. """ token["text"] = token["text"].replace("\r\n", " ").replace("\n", " ") res = render_text(token, body, stack, loop) return [urwid.Divider()] + res + [urwid.Divider()]
[docs]@tutor( "markdown", "block quote", r""" Block quotes are lines of markdown prefixed with `> `. Block quotes can contain text, other markdown, and can even be nested! <TUTOR:EXAMPLE> > Some quoted text > > > > # Heading > > > > > > > *hello world* > > > > > ~~apples~~ > > > space chips </TUTOR:EXAMPLE> ## Style Block quotes can be styled with slide metadata. This is the default style: <TUTOR:STYLE>quote</TUTOR:STYLE> """ ) @contrib_first def render_block_quote_start(token, body, stack, loop): """Begins rendering of a block quote. Pushes a new ``urwid.Pile()`` to the stack that is indented, has styling applied, and has the quote markers on the left. This function makes use of the styles: .. code-block:: yaml quote: top_corner: "┌" bottom_corner: "└" side: "╎" style: bg: default fg: italics,#aaa See :any:`lookatme.tui.SlideRenderer.do_render` for additional argument and return value descriptions. """ pile = urwid.Pile([]) stack.append(pile) styles = config.get_style()["quote"] quote_side = styles["side"] quote_top_corner = styles["top_corner"] quote_bottom_corner = styles["bottom_corner"] quote_style = styles["style"] return [ urwid.Divider(), urwid.LineBox( urwid.AttrMap( urwid.Padding(pile, left=2), utils.spec_from_style(quote_style), ), lline=quote_side, rline="", tline=" ", trcorner="", tlcorner=quote_top_corner, bline=" ", brcorner="", blcorner=quote_bottom_corner, ), urwid.Divider(), ]
[docs]@contrib_first def render_block_quote_end(token, body, stack, loop): """Pops the block quote start ``urwid.Pile()`` from the stack, taking future renderings out of the block quote styling. See :any:`lookatme.tui.SlideRenderer.do_render` for additional argument and return value descriptions. """ pile = stack.pop() # remove leading/trailing divider if they were added to the pile if isinstance(pile.contents[0][0], urwid.Divider): pile.contents = pile.contents[1:] if isinstance(pile.contents[-1][0], urwid.Divider): pile.contents = pile.contents[:-1]
[docs]@tutor( "markdown", "code blocks", r""" Multi-line code blocks are either surrounded by "fences" (three in a row of either `\`` or `~`), or are lines of text indented at least four spaces. Fenced codeblocks let you specify the language of the code: <TUTOR:EXAMPLE> ```python def hello_world(): print("Hello, world!\n") ``` </TUTOR:EXAMPLE> ## Style The syntax highlighting style used to highlight the code block can be specified in the markdown metadata: <TUTOR:STYLE>style</TUTOR:STYLE> Valid values for the `style` field come directly from pygments. In the version of pygments being used as you read this, the list of valid values is: {pygments_values} > **NOTE** This style name is confusing and will be renamed in lookatme v3.0+ """.format( pygments_values=" ".join(pygments.styles.get_all_styles()), ) ) @contrib_first def render_code(token, body, stack, loop): """Renders a code block using the Pygments library. See :any:`lookatme.tui.SlideRenderer.do_render` for additional argument and return value descriptions. """ lang = token.get("lang", "text") or "text" text = token["text"] res = pygments_render.render_text(text, lang=lang) return [ urwid.Divider(), res, urwid.Divider() ]