swh:1:snp:7ce5f1105410d5ee1ad6abfdc873986c25b579e5
Raw File
Tip revision: 7b4203f6ea65079a9a0ec1407e757c2fb19d3c10 authored by Dirk Roorda on 25 May 2023, 10:03:26 UTC
various improvements to TEI conversion
Tip revision: 7b4203f
render.py
"""
# 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 .helpers import htmlSafe, NB, dh
from .unravel import _unravel
from ..core.helpers import NBSP, 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

        dContext.set(
            "features",
            sorted(flattenToSet(extraFeatures[0]) | flattenToSet(tupleFeatures)),
        )
        dContext.set("featuresIndirect", extraFeatures[1])
        if multiFeatures:
            dContext.set("featuresAll", tuple(app.api.Fall(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 ""
    result = (
        f"""{passage}<{elem} class="{ltr} children">{rep}</{elem}>"""
        if isPretty
        else f"""<{elem} class="{ltr}{ubd}">{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,
            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,
    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/
            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>")
                material = '<span class="break"><br></span>'.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
    lookupMethod = settings.lookupMethod

    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 = lookupMethod(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 ""
    )


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
    multiFeatures = options.multiFeatures
    if multiFeatures:
        featuresAll = options.featuresAll
    queryFeatures = options.queryFeatures
    standardFeatures = options.standardFeatures
    suppress = options.suppress
    noneValues = options.noneValues
    showMath = options.showMath

    settings = info.settings
    upMethod = settings.upMethod
    lookupMethod = settings.lookupMethod

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

    # 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:
        seen = set()

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

                fsName = lookupMethod(name)
                if fsName is None:
                    continue
                fsNamev = fsName.v

                value = None
                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
                if refNode is not None:
                    value = fsNamev(refNode)

                value = (
                    None if value in noneValues else htmlEsc(value or "", math=showMath)
                )
                if value is not None:
                    value = value.replace("\n", "\\n<br>")
                    if value.endswith(" "):
                        value = value[0:-1] + NBSP
                    isBare = i < bFeatures
                    isExtra = i >= nbFeatures
                    if not multiFeatures and (
                        isExtra
                        and not queryFeatures
                        or not isExtra
                        and (not standardFeatures and name not in dFeatures)
                    ):
                        continue
                    nameRep = "" if isBare 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>"""
back to top