Skip to content

visualdynamics.report.markdown

markdown

The subset of Markdown a report's text blocks are written in.

It is written here, as the viewers are, so that no third-party code is carried inside a report. The subset is what a test report uses: headings with # to ####, bold, italic, code, bullet lists (- or *), numbered lists (1. or 1)), and paragraphs separated by blank lines. The text is HTML-escaped first, so HTML typed into a text block is shown as text, not interpreted.

Pipe tables are read when asked for (to_html(..., tables=True), which the report feature skeleton_sections does, so a section's author can type the table a section asks for): a header row, a delimiter row of dashes with optional colons for alignment, and body rows, each line opening and closing with |. A backslash before a pipe makes it part of a cell.

Functions:

Name Description
to_html

The Markdown subset in text as an HTML string.

Functions:

to_html

to_html(text: str, tables: bool = False) -> str

The Markdown subset in text as an HTML string.

A heading becomes h2 to h5, since h1 is the report's title. Lines of one paragraph are kept as lines, joined with
. With tables, pipe tables are read; without, a pipe is text like any other.

Source code in src/visualdynamics/report/markdown.py
def to_html(text: str, tables: bool = False) -> str:
    """The Markdown subset in `text` as an HTML string.

    A heading becomes h2 to h5, since h1 is the report's title. Lines of
    one paragraph are kept as lines, joined with <br>. With `tables`,
    pipe tables are read; without, a pipe is text like any other.
    """
    out, paragraph, listing = [], [], None

    def flush_paragraph() -> None:
        if paragraph:
            out.append('<p>' + '<br>'.join(paragraph) + '</p>')
            paragraph.clear()

    def flush_list() -> None:
        nonlocal listing
        if listing:
            out.append(f'</{listing}>')
            listing = None

    lines = (text or '').splitlines()
    at = 0
    while at < len(lines):
        line = lines[at]
        at += 1
        stripped = line.strip()
        aligns = (_alignments(lines[at].strip())
                  if tables and at < len(lines) and _ROW.match(stripped)
                  else None)
        if aligns is not None:
            # a header row with its delimiter under it: the table runs
            # while the lines are rows
            flush_paragraph()
            flush_list()
            rows = []
            at += 1
            while at < len(lines) and _ROW.match(lines[at].strip()):
                rows.append(_cells(lines[at]))
                at += 1
            out.append(_table(_cells(stripped), aligns, rows))
            continue
        heading = _HEADING.match(stripped)
        bullet = _BULLET.match(stripped)
        numbered = _NUMBERED.match(stripped)
        if not stripped:
            flush_paragraph()
            flush_list()
        elif heading:
            flush_paragraph()
            flush_list()
            level = len(heading.group(1)) + 1   # h1 is the report title
            out.append(f'<h{level}>{_inline(heading.group(2))}</h{level}>')
        elif bullet or numbered:
            flush_paragraph()
            wanted = 'ul' if bullet else 'ol'
            if listing != wanted:
                flush_list()
                out.append(f'<{wanted}>')
                listing = wanted
            out.append(f'<li>{_inline((bullet or numbered).group(1))}</li>')
        else:
            flush_list()
            paragraph.append(_inline(stripped))
    flush_paragraph()
    flush_list()
    return '\n'.join(out)