Java library for parsing and rendering Markdown text according to the CommonMark specification
—
Comprehensive extension framework for adding custom syntax support, parsers, and renderers to CommonMark Java. The extension system allows complete customization of parsing and rendering behavior while maintaining compatibility with the core CommonMark specification.
Base interface that all extensions must implement.
/**
* Base marker interface for extensions
* Extensions can implement multiple specific extension interfaces
*/
public interface Extension {
// Marker interface - no methods required
}Extensions that modify parsing behavior by adding custom block parsers, inline processors, and post-processors.
/**
* Extension interface for Parser
*/
public interface Parser.ParserExtension extends Extension {
/**
* Extend the parser builder with custom functionality
* @param parserBuilder the parser builder to extend
*/
void extend(Parser.Builder parserBuilder);
}Usage Examples:
import org.commonmark.Extension;
import org.commonmark.parser.Parser;
// Simple parser extension
public class MyParserExtension implements Parser.ParserExtension {
@Override
public void extend(Parser.Builder parserBuilder) {
parserBuilder.customBlockParserFactory(new MyBlockParserFactory());
parserBuilder.customDelimiterProcessor(new MyDelimiterProcessor());
parserBuilder.postProcessor(new MyPostProcessor());
}
}
// Use the extension
Parser parser = Parser.builder()
.extensions(Collections.singletonList(new MyParserExtension()))
.build();Extensions that modify rendering behavior for both HTML and text output.
/**
* Extension interface for HTML renderer
*/
public interface HtmlRenderer.HtmlRendererExtension extends Extension {
/**
* Extend the HTML renderer builder with custom functionality
* @param rendererBuilder the renderer builder to extend
*/
void extend(HtmlRenderer.Builder rendererBuilder);
}
/**
* Extension interface for text content renderer
*/
public interface TextContentRenderer.TextContentRendererExtension extends Extension {
/**
* Extend the text content renderer builder with custom functionality
* @param rendererBuilder the renderer builder to extend
*/
void extend(TextContentRenderer.Builder rendererBuilder);
}Usage Examples:
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.text.TextContentRenderer;
// HTML renderer extension
public class MyHtmlRendererExtension implements HtmlRenderer.HtmlRendererExtension {
@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
rendererBuilder.nodeRendererFactory(context -> new MyHtmlNodeRenderer(context));
rendererBuilder.attributeProviderFactory(context -> new MyAttributeProvider());
}
}
// Text renderer extension
public class MyTextRendererExtension implements TextContentRenderer.TextContentRendererExtension {
@Override
public void extend(TextContentRenderer.Builder rendererBuilder) {
rendererBuilder.nodeRendererFactory(context -> new MyTextNodeRenderer(context));
}
}
// Use the extensions
HtmlRenderer htmlRenderer = HtmlRenderer.builder()
.extensions(Collections.singletonList(new MyHtmlRendererExtension()))
.build();
TextContentRenderer textRenderer = TextContentRenderer.builder()
.extensions(Collections.singletonList(new MyTextRendererExtension()))
.build();System for adding custom block-level syntax parsing.
/**
* Factory for creating block parsers
*/
public interface BlockParserFactory {
/**
* Try to start a block parser for the current line
* @param state the parser state
* @param matchedBlockParser the matched block parser
* @return BlockStart if this factory can handle the line, null otherwise
*/
BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser);
}
/**
* Abstract base class for block parser factories
*/
public abstract class AbstractBlockParserFactory implements BlockParserFactory {
// Base implementation with utility methods
}
/**
* Parser for specific block nodes
*/
public interface BlockParser {
/**
* Whether this block can contain other blocks
* @return true if this is a container block
*/
boolean isContainer();
/**
* Whether this block can have lazy continuation lines
* @return true if lazy continuation is allowed
*/
boolean canHaveLazyContinuationLines();
/**
* Whether this block can contain the specified child block
* @param childBlock the potential child block
* @return true if the child block can be contained
*/
boolean canContain(Block childBlock);
/**
* Get the block node this parser is building
* @return the block node
*/
Block getBlock();
/**
* Try to continue parsing this block with the current line
* @param parserState the parser state
* @return BlockContinue if parsing should continue, null otherwise
*/
BlockContinue tryContinue(ParserState parserState);
/**
* Add a line to this block
* @param line the source line to add
*/
void addLine(SourceLine line);
/**
* Add a source span to this block
* @param sourceSpan the source span to add
*/
void addSourceSpan(SourceSpan sourceSpan);
/**
* Close this block (called when block parsing is complete)
*/
void closeBlock();
/**
* Parse inline content within this block
* @param inlineParser the inline parser to use
*/
void parseInlines(InlineParser inlineParser);
}
/**
* Abstract base class for block parsers
*/
public abstract class AbstractBlockParser implements BlockParser {
// Default implementations and utility methods
}Usage Examples:
import org.commonmark.parser.block.*;
import org.commonmark.node.CustomBlock;
// Custom block for code blocks with special syntax
public class MyCodeBlock extends CustomBlock {
private String language;
private String code;
// Getters and setters
}
// Block parser for custom code blocks
public class MyCodeBlockParser extends AbstractBlockParser {
private final MyCodeBlock block = new MyCodeBlock();
private final StringBuilder content = new StringBuilder();
@Override
public Block getBlock() {
return block;
}
@Override
public BlockContinue tryContinue(ParserState state) {
// Check if current line continues the block
String line = state.getLine().toString();
if (line.equals(":::")) {
return BlockContinue.finished();
}
return BlockContinue.atIndex(state.getIndex());
}
@Override
public void addLine(SourceLine line) {
content.append(line.getContent()).append('\n');
}
@Override
public void closeBlock() {
block.setCode(content.toString().trim());
}
}
// Factory for the custom block parser
public class MyCodeBlockParserFactory extends AbstractBlockParserFactory {
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
String line = state.getLine().toString();
if (line.startsWith(":::") && line.length() > 3) {
String language = line.substring(3).trim();
MyCodeBlockParser parser = new MyCodeBlockParser();
parser.getBlock().setLanguage(language);
return BlockStart.of(parser).atIndex(state.getNextNonSpaceIndex());
}
return BlockStart.none();
}
}System for adding custom inline delimiter processing (like emphasis markers).
/**
* Custom delimiter processor interface
*/
public interface DelimiterProcessor {
/**
* Get the opening delimiter character
* @return the opening character
*/
char getOpeningCharacter();
/**
* Get the closing delimiter character
* @return the closing character
*/
char getClosingCharacter();
/**
* Get the minimum length for this delimiter
* @return the minimum length
*/
int getMinLength();
/**
* Process delimiter runs
* @param openingRun the opening delimiter run
* @param closingRun the closing delimiter run
* @return the number of delimiters used, or 0 if not processed
*/
int process(DelimiterRun openingRun, DelimiterRun closingRun);
}
/**
* Information about delimiter runs during parsing
*/
public interface DelimiterRun {
/**
* Get the length of this delimiter run
* @return the length
*/
int length();
/**
* Get the text node containing the delimiters
* @return the text node
*/
Node getNode();
/**
* Whether this run can open emphasis
* @return true if can open
*/
boolean canOpen();
/**
* Whether this run can close emphasis
* @return true if can close
*/
boolean canClose();
}Usage Examples:
import org.commonmark.parser.delimiter.DelimiterProcessor;
import org.commonmark.parser.delimiter.DelimiterRun;
import org.commonmark.node.CustomNode;
// Custom inline node for subscript
public class Subscript extends CustomNode {
@Override
public void accept(Visitor visitor) {
// Handle visitor
}
}
// Delimiter processor for subscript syntax (~text~)
public class SubscriptDelimiterProcessor implements DelimiterProcessor {
@Override
public char getOpeningCharacter() {
return '~';
}
@Override
public char getClosingCharacter() {
return '~';
}
@Override
public int getMinLength() {
return 1;
}
@Override
public int process(DelimiterRun openingRun, DelimiterRun closingRun) {
if (openingRun.length() >= 1 && closingRun.length() >= 1) {
// Create subscript node
Subscript subscript = new Subscript();
// Move content between delimiters to subscript node
Node content = openingRun.getNode().getNext();
while (content != null && content != closingRun.getNode()) {
Node next = content.getNext();
subscript.appendChild(content);
content = next;
}
// Insert subscript node
openingRun.getNode().insertAfter(subscript);
return 1; // Used 1 delimiter from each run
}
return 0;
}
}System for processing nodes after parsing is complete.
/**
* Post-processes nodes after parsing
*/
public interface PostProcessor {
/**
* Process the document node after parsing is complete
* @param node the document node to process
* @return the processed node (may be the same or a new node)
*/
Node process(Node node);
}Usage Examples:
import org.commonmark.parser.PostProcessor;
import org.commonmark.node.*;
// Post-processor to add IDs to headings
public class HeadingIdPostProcessor implements PostProcessor {
@Override
public Node process(Node node) {
node.accept(new AbstractVisitor() {
@Override
public void visit(Heading heading) {
// Generate ID from heading text
String id = generateId(getTextContent(heading));
heading.setId(id);
super.visit(heading);
}
});
return node;
}
private String generateId(String text) {
return text.toLowerCase()
.replaceAll("[^a-z0-9\\s]", "")
.replaceAll("\\s+", "-")
.trim();
}
private String getTextContent(Node node) {
StringBuilder sb = new StringBuilder();
node.accept(new AbstractVisitor() {
@Override
public void visit(Text text) {
sb.append(text.getLiteral());
}
});
return sb.toString();
}
}
// Use the post-processor
Parser parser = Parser.builder()
.postProcessor(new HeadingIdPostProcessor())
.build();System for custom inline content parsing.
/**
* Factory for creating inline parsers
*/
public interface InlineParserFactory {
/**
* Create an inline parser with the given context
* @param inlineParserContext the context for inline parsing
* @return an inline parser instance
*/
InlineParser create(InlineParserContext inlineParserContext);
}
/**
* Parser for inline content
*/
public interface InlineParser {
/**
* Parse inline content within the given lines and node
* @param lines the source lines containing inline content
* @param node the parent node to add inline nodes to
*/
void parse(SourceLines lines, Node node);
}
/**
* Context for inline parsing
*/
public interface InlineParserContext {
/**
* Get the delimiter processors
* @return list of delimiter processors
*/
List<DelimiterProcessor> getCustomDelimiterProcessors();
/**
* Get link reference definitions
* @return map of link reference definitions
*/
Map<String, LinkReferenceDefinition> getLinkReferenceDefinitions();
}Here's a complete example that demonstrates creating a comprehensive extension:
import org.commonmark.Extension;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.text.TextContentRenderer;
/**
* Complete extension for adding alert blocks syntax
* Syntax: !!! type "title"
* content
* !!!
*/
public class AlertExtension implements
Parser.ParserExtension,
HtmlRenderer.HtmlRendererExtension,
TextContentRenderer.TextContentRendererExtension {
// Create extension instance
public static Extension create() {
return new AlertExtension();
}
@Override
public void extend(Parser.Builder parserBuilder) {
parserBuilder.customBlockParserFactory(new AlertBlockParserFactory());
}
@Override
public void extend(HtmlRenderer.Builder rendererBuilder) {
rendererBuilder.nodeRendererFactory(context -> new AlertHtmlRenderer(context));
}
@Override
public void extend(TextContentRenderer.Builder rendererBuilder) {
rendererBuilder.nodeRendererFactory(context -> new AlertTextRenderer(context));
}
}
// Use the complete extension
List<Extension> extensions = Collections.singletonList(AlertExtension.create());
Parser parser = Parser.builder()
.extensions(extensions)
.build();
HtmlRenderer htmlRenderer = HtmlRenderer.builder()
.extensions(extensions)
.build();
TextContentRenderer textRenderer = TextContentRenderer.builder()
.extensions(extensions)
.build();
// Parse and render alert blocks
String markdown = "!!! warning \"Important\"\nThis is a warning message.\n!!!";
Node document = parser.parse(markdown);
String html = htmlRenderer.render(document);
String text = textRenderer.render(document);Create extensions that work with both parser and renderers for complete functionality.
Extend AbstractBlockParser, AbstractBlockParserFactory for easier implementation.
Implement both HTML and text rendering for custom nodes.
Ensure extensions don't break existing CommonMark functionality.
Use static factory methods like create() for extension instantiation.
Clearly document the syntax and behavior of custom extensions.
Install with Tessl CLI
npx tessl i tessl/maven-org-commonmark--commonmark