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 &}, 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 + ";");
}
}
}