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("--", "–")
.entity("---", "—")
.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);
}
}