NamedEntityNodeRenderer.java

/*
 * Copyright © 2026 The CTAN Team and individual authors
 *
 * This file is distributed under the 3-clause BSD license.
 * See file LICENSE for details.
 */

package org.ctan.markup.gfm.ext;

import java.util.Map;
import java.util.Set;

import org.commonmark.node.Node;
import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlWriter;

/**
 * Renders {@link NamedEntityNode} instances to HTML.
 *
 * <p>
 * For each node the renderer looks up the entity name in the configured map:
 * <ul>
 * <li>If a mapping exists the raw HTML value is written directly
 * (unescaped).</li>
 * <li>Otherwise the original {@code &Name;} literal is written as escaped text
 * so it appears verbatim in the browser.</li>
 * </ul>
 *
 *
 *
 *
 *
 * <p>
 * The renderer is registered with the
 * {@link org.commonmark.renderer.html.HtmlRenderer} by
 * {@link NamedEntityExtension} via an
 * {@link org.commonmark.renderer.html.HtmlNodeRendererFactory}. It is the
 * render-time counterpart to {@link NamedEntityInlineParser}, which produces
 * the AST nodes this class consumes.
 *
 * <p>
 * For each {@link NamedEntityNode} the renderer looks up the entity name in the
 * configured map:
 * <ul>
 * <li>If a mapping exists, the raw HTML value is written directly via
 * {@link HtmlWriter#raw}, so it may contain arbitrary markup (e.g. nested
 * {@code <span>} elements for typographic logos).</li>
 * <li>If no mapping exists, the original {@code &Name;} literal is written via
 * {@link HtmlWriter#text}, which HTML-escapes the ampersand, so the entity
 * appears verbatim in the browser rather than being misinterpreted.</li>
 * </ul>
 *
 * <p>
 * This class is package-private; obtain instances through
 * {@link NamedEntityExtension.Builder#build()}.
 */
final class NamedEntityNodeRenderer implements NodeRenderer {

    /**
     * Writer used to emit HTML into the current rendering context. Obtained
     * from {@link HtmlNodeRendererContext#getWriter()} at construction time.
     */
    private final HtmlWriter html;

    /**
     * Unmodifiable map of entity name → raw HTML replacement string, shared
     * with the owning {@link NamedEntityExtension}.
     */
    private final Map<String, String> entityMap;

    /**
     * Constructs a renderer for the given rendering context and entity map.
     *
     * @param ctx the HTML rendering context supplied by the commonmark
     *     framework; used to obtain the {@link HtmlWriter}
     * @param entityMap an unmodifiable map of entity name (e.g. {@code "TeX"})
     *     to the raw HTML string that should be emitted in its place
     */
    NamedEntityNodeRenderer(HtmlNodeRendererContext ctx,
        Map<String, String> entityMap) {

        this.html = ctx.getWriter();
        this.entityMap = entityMap;
    }

    /**
     * Returns the singleton set containing {@link NamedEntityNode}, the only
     * AST node type this renderer handles.
     *
     * @return an immutable set containing {@code NamedEntityNode.class}
     */
    @Override
    public Set<Class<? extends Node>> getNodeTypes() {

        return Set.of(NamedEntityNode.class);
    }

    /**
     * Renders a single {@link NamedEntityNode} to HTML.
     *
     * <p>
     * The node's {@linkplain NamedEntityNode#getName() name} is looked up in
     * the entity map:
     * <ul>
     * <li><b>Known entity</b> — the mapped HTML string is written verbatim via
     * {@link HtmlWriter#raw}. The caller is responsible for ensuring the value
     * is well-formed HTML; no additional escaping is applied.</li>
     * <li><b>Unknown entity</b> — the reconstructed {@code &Name;} literal is
     * written via {@link HtmlWriter#text}, which escapes the {@code &} as
     * {@code &amp;}, preserving the original source text in the output.</li>
     * </ul>
     *
     * @param node the AST node to render; must be an instance of
     *     {@link NamedEntityNode}
     */
    @Override
    public void render(Node node) {

        NamedEntityNode entityNode = (NamedEntityNode) node;
        String name = entityNode.getName();
        String replacement = entityMap.get(name);

        if (replacement != null) {
            // Emit the configured HTML verbatim (it may contain tags).
            html.raw(replacement);
        } else {
            // Unknown entity — preserve the original source text, HTML-escaped.
            html.text("&" + name + ";");
        }
    }
}