Module tf.advanced.render

Render

Rendering is the process of generating HTML for a node, taking into account display options (tf.advanced.options) and app settings (tf.advanced.settings).

It is organized as an unravel step (tf.advanced.unravel), that generates a tree of node fragments followed by an HTML generating step, that generates HTML for a tree in a recursive way.

The unravel step retrieves all relevant settings and options and stores them in the tree in such a way that the essential information for rendering a subtree is readily available at the top of that subtree.

Information shielding

The recursive render step does not have to consult the app object anymore, because all information it needs from the app object is stored in the tree, and all methods that need to be invoked on the app object are also accessible directly from an attribute in the tree.

Expand source code Browse git
"""
# Render

Rendering is the process of generating HTML for a node, taking into account
display options (`tf.advanced.options`) and app settings (`tf.advanced.settings`).

It is organized as an *unravel* step (`tf.advanced.unravel`),
that generates a tree of node fragments
followed by an HTML generating step, that generates HTML for a tree in a recursive way.

The *unravel* step retrieves all relevant settings and options and stores them
in the tree in such a way that the essential information for rendering a subtree
is readily available at the top of that subtree.

## Information shielding

The recursive render step does not have to consult the `app` object anymore,
because all information it needs from the `app` object is stored in the tree,
and all methods that need to be invoked on the `app` object are also accessible
directly from an attribute in the tree.
"""

import re
from textwrap import dedent

from .helpers import htmlSafe, NB, dh
from .highlight import getEdgeHlAtt
from .unravel import _unravel
from ..core.helpers import NBSP, TO_SYM, FROM_SYM, htmlEsc, flattenToSet


def render(app, isPretty, n, _inTuple, _asString, explain, **options):
    """Renders a node, in plain or pretty mode.

    We take care that when a node has graphics, and the node is split into several
    chunks / fragments, the graphics only occurs on the first fragment.
    """

    graphicsFetched = set()
    inNb = app.inNb
    display = app.display

    if not display.check("pretty" if isPretty else "plain", options):
        return ""

    _browse = app._browse

    dContext = display.distill(options)

    if isPretty:
        tupleFeatures = dContext.tupleFeatures
        extraFeatures = dContext.extraFeatures
        multiFeatures = dContext.multiFeatures
        queryFeatures = dContext.queryFeatures

        dContext.set(
            "features",
            sorted(
                flattenToSet(extraFeatures[0])
                | (flattenToSet(tupleFeatures) if queryFeatures else set())
            ),
        )
        dContext.set("featuresIndirect", extraFeatures[1])
        if multiFeatures:
            api = app.api
            Fall = api.Fall
            Eall = api.Eall
            dContext.set(
                "featuresAll", tuple(Fall(warp=False)) + tuple(Eall(warp=False))
            )

    tree = _unravel(app, isPretty, dContext, n, _inTuple=_inTuple, explain=explain)
    (chunk, info, subTrees) = tree
    settings = info.settings

    passage = _getPassage(isPretty, info, n)

    html = []

    for subTree in subTrees:
        _render(isPretty, subTree, True, True, 0, passage, html, graphicsFetched)

    rep = "".join(html)
    ltr = settings.ltr

    elem = "span" if _inTuple else "div"
    ubd = " ubd" if _inTuple else ""
    kindRep = "pr-mode" if isPretty else "pl-mode"
    result = (
        f"""{passage}<{elem} class="{ltr} children {kindRep}">{rep}</{elem}>"""
        if isPretty
        else f"""<{elem} class="{ltr}{ubd} {kindRep}">{passage}{rep}</{elem}>"""
    )

    if _browse or _asString:
        return result
    dh(result, inNb=inNb)


def _render(
    isPretty,
    tree,
    first,
    last,
    level,
    passage,
    html,
    graphicsFetched,
    switched=False,
    _asString=False,
):
    outer = level == 0
    (chunk, info, children) = tree
    (n, (b, e)) = chunk
    settings = info.settings
    props = info.props
    boundaryCls = info.boundaryCls

    ltr = settings.ltr
    isBaseNonSlot = props.isBaseNonSlot
    plainCustom = props.plainCustom

    if isPretty:
        nodePlain = None
        if isBaseNonSlot:
            nodePlain = _render(
                False,
                tree,
                first,
                last,
                level,
                "",
                [],
                graphicsFetched,
                switched=True,
                _asString=True,
            )
        (label, featurePart) = _prettyTree(tree, outer, first, last, level, nodePlain)
        (containerB, containerE) = _prettyPre(
            tree,
            outer,
            label,
            featurePart,
            boundaryCls,
            html,
            graphicsFetched,
        )
        cls = props.cls
        childCls = cls["children"]

        if children and not isBaseNonSlot:
            html.append(f'<div class="{childCls} {ltr}">')
            after = props.after
    else:
        (contribB, contribE) = _plainPre(info, n, boundaryCls, outer, switched)
        contrib = _plainTree(
            contribB,
            contribE,
            tree,
            outer,
            first,
            last,
            level,
            boundaryCls,
            passage,
            graphicsFetched,
        )
        if contrib:
            html.append(contrib)

    lastCh = len(children) - 1

    if not ((isPretty and isBaseNonSlot) or (not isPretty and plainCustom)):
        for i, subTree in enumerate(children):
            thisFirst = first and i == 0
            thisLast = last and i == lastCh
            _render(
                isPretty,
                subTree,
                thisFirst,
                thisLast,
                level + 1,
                "",
                html,
                graphicsFetched,
            )
            if isPretty and after:
                html.append(after(subTree[0][0]))

    if isPretty:
        if children and not isBaseNonSlot:
            html.append("</div>")
        _prettyPost(label, featurePart, html, containerB, containerE)
    else:
        _plainPost(contribE, html)

    return "".join(html) if outer or _asString else None


# PLAIN LOW-LEVEL


def _plainPre(info, n, boundaryCls, outer, switched):
    isPretty = False
    options = info.options
    plainGaps = options.plainGaps

    settings = info.settings
    ltr = settings.ltr

    props = info.props
    hlCls = props.hlCls[isPretty]
    hlStyle = props.hlStyle[isPretty]

    nodePart = _getNodePart(False, info, n, outer, switched)

    boundary = boundaryCls if plainGaps else ""
    theHlCls = "" if switched else hlCls
    theHlStyle = "" if switched else hlStyle
    if boundary in {"r", "l"} or theHlCls or theHlStyle or nodePart or switched:
        clses = f"plain {ltr} {boundary} {theHlCls}"
        contribB = f'<span class="{clses}" {theHlStyle}>'
        contribE = "</span>"
    else:
        contribB = ""
        contribE = ""
    if nodePart:
        contribB += nodePart
    return (contribB, contribE)


def _plainPost(contribE, html):
    if contribE:
        html.append(contribE)


SPAN_RE = re.compile(r"^(<span\b[^>]*>)(.*)(</span>)$", re.S)


def _plainTree(
    contribB,
    contribE,
    tree,
    outer,
    first,
    last,
    level,
    boundaryCls,
    passage,
    graphicsFetched,
):
    (chunk, info, subTrees) = tree

    options = info.options
    isHtml = options.isHtml
    fmt = options.fmt
    showGraphics = options.showGraphics
    showMath = options.showMath

    settings = info.settings
    textMethod = settings.textMethod
    ltr = settings.ltr
    getText = settings.getText
    getGraphics = settings.getGraphics

    props = info.props
    hasGraphics = props.hasGraphics
    textCls = props.textCls
    nType = props.nType
    isSlotOrDescend = props.isSlotOrDescend
    descend = props.descend
    plainCustom = props.plainCustom

    chunk = tree[0]
    n = chunk[0]

    if showGraphics and hasGraphics and n not in graphicsFetched:
        graphics = getGraphics(False, n, nType, outer)
        graphicsFetched.add(n)
    else:
        graphics = ""

    contrib = ""

    if plainCustom is not None:
        contrib = plainCustom(options, chunk, nType, outer)
        return contribB + contrib + graphics

    if isSlotOrDescend:
        text = textMethod(
            n,
            fmt=fmt,
            descend=descend,
            outer=outer,
            first=first,
            last=last,
            level=level,
        )
        if text:
            material = htmlSafe(text, isHtml, math=showMath)
            cb = f'<span class="{textCls}">'
            ce = "</span>"

            # a <br> in flex box has no effect
            # so we create a "breaking" span by setting the width to 100% and
            # the height to 0
            # See https://tobiasahlin.com/blog/flexbox-break-to-new-row/
            # We might have to dig one level of spans deeper if contribB is not empty
            if "<br>" in material:
                match = SPAN_RE.match(material)
                if match:
                    (start, content, end) = match.group(1, 2, 3)
                else:
                    (start, content, end) = ("", material, "")
                parts = content.split("<br>")
                joinerBase = '<span class="break"><br></span>'
                joiner = (
                    joinerBase
                    if contribB == ""
                    else f"{contribE}{joinerBase}{contribB}"
                )
                material = joiner.join(
                    f"{cb}{start}{part}{end}{ce}" for part in parts
                )
                contrib = material
            else:
                contrib = f"{cb}{material}{ce}"
    else:
        tplFilled = getText(
            False,
            n,
            nType,
            outer,
            first,
            last,
            level,
            passage if outer else "",
            descend,
            options=options,
        )
        if tplFilled:
            contrib = f'<span class="{textCls} {ltr}">{tplFilled}</span>'

    return contribB + contrib + graphics


# PRETTY LOW-LEVEL


def _prettyPre(tree, outer, label, featurePart, boundaryCls, html, graphicsFetched):
    isPretty = True
    (chunk, info, subTrees) = tree
    n = chunk[0]

    options = info.options
    showGraphics = options.showGraphics

    settings = info.settings
    getGraphics = settings.getGraphics
    ltr = settings.ltr

    props = info.props
    hasGraphics = props.hasGraphics
    nType = props.nType
    cls = props.cls
    isBaseNonSlot = props.isBaseNonSlot
    hlCls = props.hlCls[isPretty]
    hlStyle = props.hlStyle[isPretty]

    contCls = cls["container"]
    label0 = label.get("", None)
    labelB = label.get("b", None)

    n = tree[0][0]

    containerB = f'<div class="{contCls} {{}} {ltr} {boundaryCls} {hlCls}" {hlStyle}>'
    containerE = "</div>"

    terminalCls = "trm"
    material = featurePart
    if labelB is not None:
        trm = terminalCls
        html.append(f"{containerB.format(trm)}{labelB}{material}{containerE}")
    if label0 is not None:
        trm = terminalCls if isBaseNonSlot or not subTrees else ""
        html.append(f"{containerB.format(trm)}{label0}{material}")

    if showGraphics and hasGraphics and n not in graphicsFetched:
        html.append(getGraphics(True, n, nType, outer))
        graphicsFetched.add(n)

    return (containerB, containerE)


def _prettyPost(label, featurePart, html, containerB, containerE):
    label0 = label.get("", None)
    labelE = label.get("e", None)

    if label0 is not None:
        html.append(containerE)
    if labelE is not None:
        html.append(f"{containerB}{labelE} {featurePart}{containerE}")


def _prettyTree(tree, outer, first, last, level, nodePlain):
    isPretty = True
    (chunk, info, subTrees) = tree
    n = chunk[0]

    options = info.options

    settings = info.settings
    upMethod = settings.upMethod
    slotsMethod = settings.slotsMethod
    webLink = settings.webLink
    getText = settings.getText

    props = info.props
    nType = props.nType
    cls = props.cls
    hlCls = props.hlCls[isPretty]
    hlStyle = props.hlStyle[isPretty]
    descend = props.descend
    isBaseNonSlot = props.isBaseNonSlot
    isLexType = props.isLexType
    lexType = props.lexType
    textCls = props.textCls

    nodePart = _getNodePart(True, info, n, outer, False)
    labelHlCls = hlCls
    labelHlStyle = hlStyle

    if isBaseNonSlot:
        heading = nodePlain
    else:
        heading = getText(
            True, n, nType, outer, first, last, level, "", descend, options=options
        )

    heading = f'<span class="{textCls}">{heading}</span>' if heading else ""

    featurePart = _getFeatures(info, n, nType)

    if isLexType:
        slots = slotsMethod(n)
        extremeOccs = (slots[0],) if len(slots) == 1 else (slots[0], slots[-1])
        linkOccs = " - ".join(webLink(lo, _asString=True) for lo in extremeOccs)
        featurePart += f'<div class="occs">{linkOccs}</div>'
    if lexType:
        lx = upMethod(n, otype=lexType)
        if lx:
            heading = webLink(lx[0], heading, _asString=True)

    label = {}
    for x in ("", "b", "e"):
        key = f"label{x}"
        if key in cls:
            val = cls[key]
            terminalCls = "trm" if x or isBaseNonSlot or not subTrees else ""
            sep = " " if nodePart and heading else ""
            material = f"{nodePart}{sep}{heading}" if nodePart or heading else ""
            label[x] = (
                f'<div class="{val} {terminalCls} {labelHlCls}" {labelHlStyle}>'
                f"{material}</div>"
                if material
                else ""
            )

    return (label, featurePart)


def _getPassage(isPretty, info, n):
    options = info.options
    withPassage = options.withPassage

    settings = info.settings
    webLink = settings.webLink

    if not withPassage:
        return ""

    ltr = settings.ltr

    passage = webLink(n, _asString=True)
    wrap = "div" if isPretty else "span"
    sep = "" if isPretty else NB * 2
    return (
        f"""<{wrap} class="tfsechead {ltr}">"""
        f"""<span class="ltr">{passage}</span></{wrap}>{sep}"""
    )


def _getNodePart(isPretty, info, n, outer, switched):
    options = info.options
    withNodes = options.withNodes and not switched
    withTypes = options.withTypes and not switched
    prettyTypes = options.prettyTypes and not switched
    lineNumbers = options.lineNumbers and not switched

    settings = info.settings
    browsing = settings.browsing
    fLookupMethod = settings.fLookupMethod

    props = info.props
    nType = props.nType
    isSlot = props.isSlot
    hlCls = props.hlCls[isPretty]
    lineNumberFeature = props.lineNumberFeature

    allowInfo = isPretty or (outer and not switched) or hlCls != ""

    num = ""
    if withNodes and allowInfo:
        num = n

    ntp = ""
    if (withTypes or isPretty and prettyTypes) and not isSlot and allowInfo:
        ntp = nType

    line = ""
    if lineNumbers and allowInfo:
        if lineNumberFeature:
            line = fLookupMethod(lineNumberFeature).v(n)
        if line:
            line = f"@{line}" if line else ""

    elemb = 'a href="#"' if browsing else "span"
    eleme = "a" if browsing else "span"
    sep = ":" if ntp and num else ""

    return (
        f'<{elemb} class="nd">{ntp}{sep}{num}{line} </{eleme}>'
        if ntp or num or line
        else ""
    )


TO_SYM_WRAPPED = f'<span class="etfx">{TO_SYM}</span>'
FROM_SYM_WRAPPED = f'<span class="etfx">{FROM_SYM}</span>'


def _getEdge(e, n, kv, withNodes, right, highlights):
    (m, val) = kv if type(kv) is tuple else (kv, None)
    pair = (n, m) if right else (m, n)

    (hlCls, hlStyle) = getEdgeHlAtt(e, pair, highlights)

    nodeRep = f'<span class="nde">{m}</span>' if withNodes else ""
    valRep = "" if val is None else htmlEsc(val)
    plainValue = (
        f"{valRep}{TO_SYM_WRAPPED}{nodeRep}"
        if right
        else f"{nodeRep}{FROM_SYM_WRAPPED}{valRep}"
    )
    arrow = "right" if right else "left"
    sep = " " if hlCls else ""

    return dedent(
        f"""
        <span
            ef="{e}"
            nd="{n}"
            md="{m}"
            arrow="{arrow}"
            class="etf{sep}{hlCls}" {hlStyle}
        >{plainValue}</span>
        """
    )


def _getFeatures(info, n, nType):
    """Feature fetcher.

    Helper for `pretty` that wraps the requested features and their values for
    *node* in HTML for pretty display.
    """

    options = info.options
    dFeatures = options.features
    dFeaturesIndirect = options.featuresIndirect
    edgeFeatures = options.edgeFeatures
    forceEdges = options.forceEdges
    multiFeatures = options.multiFeatures

    if multiFeatures:
        featuresAll = options.featuresAll

    # queryFeatures = options.queryFeatures
    tupleFeatures = options.tupleFeatures
    standardFeatures = options.standardFeatures
    suppress = options.suppress
    noneValues = options.noneValues
    showMath = options.showMath
    withNodes = options.withNodes
    edgeHighlights = options.edgeHighlights

    settings = info.settings
    upMethod = settings.upMethod
    fLookupMethod = settings.fLookupMethod
    eLookupMethod = settings.eLookupMethod
    allEFeats = settings.allEFeats

    props = info.props
    (features, indirect) = props.features
    (featuresBare, indirectBare) = props.featuresBare

    if forceEdges:
        newDFeatures = []
        seen = set()
        for f in dFeatures + list(allEFeats):
            if f in allEFeats:
                if f in edgeFeatures:
                    if f not in seen:
                        newDFeatures.append(f)
                        seen.add(f)
                else:
                    continue
            else:
                newDFeatures.append(f)
        dFeatures = newDFeatures

    # a feature can be nType:feature
    # do a upMethod(n, otype=nType)[0] and take the feature from there

    givenFeatureSet = set(features) | set(featuresBare)
    xFeatures = tuple(
        f for f in dFeatures if not standardFeatures or f not in givenFeatureSet
    )
    featureList = tuple(featuresBare + features) + xFeatures
    if multiFeatures:
        featureList += featuresAll
    bFeatures = len(featuresBare)
    nbFeatures = len(featuresBare) + len(features)

    featurePart = ""

    # if standardFeatures or queryFeatures or multiFeatures or forceEdges:
    if standardFeatures or tupleFeatures or multiFeatures or forceEdges:
        seen = set()

        for i, name in enumerate(featureList):
            if name not in suppress and name not in seen:
                seen.add(name)

                if (
                    name in dFeaturesIndirect
                    or name in indirectBare
                    or name in indirect
                ):
                    refType = (
                        dFeaturesIndirect[name]
                        if name in dFeaturesIndirect
                        else indirectBare[name]
                        if name in indirectBare
                        else indirect[name]
                    )
                    refNode = upMethod(n, otype=refType)
                    refNode = refNode[0] if refNode else None
                else:
                    refNode = n

                value = None

                if refNode is not None:
                    if name in allEFeats:
                        esObj = eLookupMethod(name, warn=False)
                        valueF = esObj.f(refNode)
                        valueT = esObj.t(refNode)
                        eHighlights = (
                            None
                            if edgeHighlights is None
                            else edgeHighlights.get(name, None)
                        )
                        if len(valueF):
                            valueF = " ".join(
                                _getEdge(
                                    name, refNode, it, withNodes, True, eHighlights
                                )
                                for it in valueF
                            )
                        if len(valueT):
                            valueT = " ".join(
                                _getEdge(
                                    name, refNode, it, withNodes, False, eHighlights
                                )
                                for it in valueT
                            )
                        value = (
                            None
                            if not len(valueF) and not len(valueT)
                            else (valueT or "") + (valueF or "")
                        )
                    else:
                        fsObj = fLookupMethod(name, warn=False)
                        value = fsObj.v(refNode)
                        if value in noneValues:
                            value = None
                        else:
                            value = htmlEsc(value, math=showMath)

                if value is not None:
                    if name not in allEFeats:
                        value = value.replace("\n", "\\n<br>")
                        if value.endswith(" "):
                            value = value[0:-1] + NBSP
                    isBare = i < bFeatures
                    isExtra = i >= nbFeatures
                    if (
                        not multiFeatures
                        and not (isExtra and forceEdges and name in edgeFeatures)
                        and (
                            # (isExtra and not queryFeatures)
                            # isExtra
                            # or (
                            not isExtra
                            and (not standardFeatures and name not in dFeatures)
                            # )
                        )
                    ):
                        continue
                    nameRep = (
                        ""
                        if isBare
                        else (
                            (
                                f'<span class="e" edge="{name}" nd="{refNode}">'
                                f"{name}•</span>"
                            )
                            if name in allEFeats
                            else f'<span class="f">{name}=</span>'
                        )
                    )
                    titleRep = f'title="{name}"' if isBare else ""
                    xCls = "xft" if isExtra else ""
                    featurePart += (
                        f'<span class="{name.lower()} {xCls}" {titleRep}>'
                        f"{nameRep}{value}</span>"
                    )
    if not featurePart:
        return ""

    return f"""<div class="features">{featurePart}</div>"""

Functions

def render(app, isPretty, n, _inTuple, _asString, explain, **options)

Renders a node, in plain or pretty mode.

We take care that when a node has graphics, and the node is split into several chunks / fragments, the graphics only occurs on the first fragment.