GfmToHtml.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;

import java.util.List;

import org.commonmark.ext.autolink.AutolinkExtension;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.ext.task.list.items.TaskListItemsExtension;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.ctan.markup.Logos;
import org.ctan.markup.gfm.ext.NamedEntityExtension;

/**
 * Converts GitHub Flavored Markdown (GFM) to HTML using the commonmark-java
 * library.
 *
 * <p>
 * Supported GFM features:
 * <ul>
 * <li>Tables</li>
 * <li>Strikethrough (~~text~~)</li>
 * <li>Autolinks (bare URLs)</li>
 * <li>Task list items (- [x] / - [ ])</li>
 * <li>Named HTML entity translation via {@link NamedEntityExtension}</li>
 * </ul>
 *
 * <p>
 * Usage: <pre>
 *   # Fragment (no HTML boilerplate):
 *   java -jar gfm-to-html.jar input.md
 *
 *   # Full HTML document written to a file:
 *   java -jar gfm-to-html.jar input.md output.html
 * </pre>
 */
public class GfmToHtml {

    /**
     * The method <code>defaultEntityExtension</code> returns a
     * {@link NamedEntityExtension} pre-loaded with common TeX-family logos.
     * Override by calling {@link #GfmToHtml(NamedEntityExtension)} with a
     * custom-built extension.
     *
     * @return a NamedEntityExtension with the predefined TeX logos
     */
    private static NamedEntityExtension defaultEntityExtension() {

        return NamedEntityExtension.builder()
            .entity("AmSLaTeX", Logos.AMS_LATEX)
            .entity("AmSTeX", Logos.AMS_TEX)
            .entity("BibTeX", Logos.BIBTEX)
            .entity("ConTeXt", Logos.CONTEXT)
            .entity("e-TeX", Logos.ETEX)
            .entity("emTeX", Logos.EMTEX)
            .entity("(La)TeX", Logos._LA_TEX)
            .entity("LaTeX", Logos.LATEX)
            .entity("LaTeX3", Logos.LATEX3)
            .entity("LaTeXe", Logos.LATEX2E)
            .entity("LaTeX2e", Logos.LATEX2E)
            .entity("LaTeX(2e)", Logos.LATEX_2E_)
            .entity("LuaTeX", Logos.LUATEX)
            .entity("LuaLaTeX", Logos.LUALATEX)
            .entity("LyX", Logos.LYX)
            .entity("Metafont", Logos.METAFONT)
            .entity("MetaFont", Logos.METAFONT)
            .entity("Metapost", Logos.METAPOST)
            .entity("MetaPost", Logos.METAPOST)
            .entity("MikTeX", Logos.MIKTEX)
            .entity("MiKTeX", Logos.MIKTEX)
            .entity("pdfLaTeX", Logos.PDFLATEX)
            .entity("PdfLaTeX", Logos.PDFLATEX)
            .entity("pdfTeX", Logos.PDFTEX)
            .entity("PdfTeX", Logos.PDFTEX)
            .entity("PicTeX", Logos.PICTEX)
            .entity("SliTeX", Logos.SLITEX)
            .entity("TeX", Logos.TEX)
            .entity("teTeX", Logos.TETEX)
            .entity("XeTeX", Logos.XETEX)
            .entity("XeLaTeX", Logos.XELATEX)
            .entity("Xe(La)TeX", Logos.XE_LA_TEX)
            .entity("--", "&ndash;")
            .entity("---", "&mdash;")
            .replaceLogoText(true)
            .build();
    }

    private final Parser parser;

    private final HtmlRenderer renderer;

    /**
     * Constructs a converter with all GFM extensions enabled, plus the default
     * named-entity mappings ({@code &TeX;}, {@code &LaTeX;}, {@code &BibTeX;}).
     */
    public GfmToHtml() {

        var extensions = List.of(
            defaultEntityExtension(),
            TablesExtension.create(),
            StrikethroughExtension.create(),
            AutolinkExtension.create(),
            TaskListItemsExtension.create());
        this.parser = Parser.builder()
            .extensions(extensions)
            .build();
        this.renderer = HtmlRenderer.builder()
            .extensions(extensions)
            .build();
    }

    /**
     * Converts a GFM string to an HTML fragment (no {@code <html>} wrapper).
     *
     * @param markdown the GFM source text
     * @return the rendered HTML fragment
     */
    public String process(String markdown) {

        Node document = parser.parse(markdown);
        return renderer.render(document);
    }
}