diff --git a/README.md b/README.md
index 8967643e4..c0347d34f 100644
--- a/README.md
+++ b/README.md
@@ -143,6 +143,26 @@ Enables tables using pipes as in [GitHub Flavored Markdown][gfm-tables].
Use class `TablesExtension` in artifact `commonmark-ext-gfm-tables`.
+### YAML front matter
+
+Enables an YAML front matter block. This extension only supports a subset of YAML syntax. Here's an example of what's supported:
+
+```
+---
+key: value
+list:
+ - value 1
+ - value 2
+literal: |
+ this is literal value.
+
+ literal values 2
+---
+
+document start here
+```
+
+Use class `YamlFrontMatterExtension` in artifact `commonmark-ext-yaml-front-matter`. To fetch metadata, use `YamlFrontMatterVisitor`.
Contributing
------------
diff --git a/commonmark-ext-yaml-front-matter/pom.xml b/commonmark-ext-yaml-front-matter/pom.xml
new file mode 100644
index 000000000..ada9956bc
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+
+ commonmark-parent
+ com.atlassian.commonmark
+ 0.4.2-SNAPSHOT
+
+
+ commonmark-ext-yaml-front-matter
+ commonmark-java extension for YAML front matter
+ commonmark-java extension for YAML front matter
+
+
+
+ com.atlassian.commonmark
+ commonmark
+
+
+
+ com.atlassian.commonmark
+ commonmark-test-util
+ test
+
+
+
+
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterBlock.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterBlock.java
new file mode 100644
index 000000000..0d9aba2d3
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterBlock.java
@@ -0,0 +1,6 @@
+package org.commonmark.ext.front.matter;
+
+import org.commonmark.node.CustomBlock;
+
+public class YamlFrontMatterBlock extends CustomBlock {
+}
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterExtension.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterExtension.java
new file mode 100644
index 000000000..ec01b3194
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterExtension.java
@@ -0,0 +1,37 @@
+package org.commonmark.ext.front.matter;
+
+import org.commonmark.Extension;
+import org.commonmark.ext.front.matter.internal.YamlFrontMatterBlockParser;
+import org.commonmark.ext.front.matter.internal.YamlFrontMatterBlockRenderer;
+import org.commonmark.html.HtmlRenderer;
+import org.commonmark.parser.Parser;
+
+/**
+ * Extension for YAML-like metadata.
+ *
+ * Create it with {@link #create()} and then configure it on the builders
+ * ({@link org.commonmark.parser.Parser.Builder#extensions(Iterable)},
+ * {@link org.commonmark.html.HtmlRenderer.Builder#extensions(Iterable)}).
+ *
+ *
+ * The parsed metadata is turned into {@link YamlFrontMatterNode}. You can access the metadata using {@link YamlFrontMatterVisitor}.
+ *
+ */
+public class YamlFrontMatterExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
+ private YamlFrontMatterExtension() {
+ }
+
+ @Override
+ public void extend(HtmlRenderer.Builder rendererBuilder) {
+ rendererBuilder.customHtmlRenderer(new YamlFrontMatterBlockRenderer());
+ }
+
+ @Override
+ public void extend(Parser.Builder parserBuilder) {
+ parserBuilder.customBlockParserFactory(new YamlFrontMatterBlockParser.Factory());
+ }
+
+ public static Extension create() {
+ return new YamlFrontMatterExtension();
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterNode.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterNode.java
new file mode 100644
index 000000000..20eb3baf7
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterNode.java
@@ -0,0 +1,31 @@
+package org.commonmark.ext.front.matter;
+
+import org.commonmark.node.CustomNode;
+
+import java.util.List;
+
+public class YamlFrontMatterNode extends CustomNode {
+ private String key;
+ private List values;
+
+ public YamlFrontMatterNode(String key, List values) {
+ this.key = key;
+ this.values = values;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public List getValues() {
+ return values;
+ }
+
+ public void setValues(List values) {
+ this.values = values;
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterVisitor.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterVisitor.java
new file mode 100644
index 000000000..1c23966f5
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/YamlFrontMatterVisitor.java
@@ -0,0 +1,29 @@
+package org.commonmark.ext.front.matter;
+
+import org.commonmark.node.AbstractVisitor;
+import org.commonmark.node.CustomNode;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class YamlFrontMatterVisitor extends AbstractVisitor {
+ private Map> data;
+
+ public YamlFrontMatterVisitor() {
+ data = new LinkedHashMap<>();
+ }
+
+ @Override
+ public void visit(CustomNode customNode) {
+ if (customNode instanceof YamlFrontMatterNode) {
+ data.put(((YamlFrontMatterNode) customNode).getKey(), ((YamlFrontMatterNode) customNode).getValues());
+ } else {
+ super.visit(customNode);
+ }
+ }
+
+ public Map> getData() {
+ return data;
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockParser.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockParser.java
new file mode 100644
index 000000000..4de4ae12f
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockParser.java
@@ -0,0 +1,118 @@
+package org.commonmark.ext.front.matter.internal;
+
+import org.commonmark.ext.front.matter.YamlFrontMatterBlock;
+import org.commonmark.ext.front.matter.YamlFrontMatterNode;
+import org.commonmark.internal.DocumentBlockParser;
+import org.commonmark.node.Block;
+import org.commonmark.parser.InlineParser;
+import org.commonmark.parser.block.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class YamlFrontMatterBlockParser extends AbstractBlockParser {
+ private static final Pattern REGEX_METADATA = Pattern.compile("^[ ]{0,3}([A-Za-z0-9_-]+):\\s*(.*)");
+ private static final Pattern REGEX_METADATA_LIST = Pattern.compile("^[ ]+-\\s*(.*)");
+ private static final Pattern REGEX_METADATA_LITERAL = Pattern.compile("^\\s*(.*)");
+ private static final Pattern REGEX_BEGIN = Pattern.compile("^-{3}(\\s.*)?");
+ private static final Pattern REGEX_END = Pattern.compile("^(-{3}|\\.{3})(\\s.*)?");
+
+ private boolean inYAMLBlock;
+ private boolean inLiteral;
+ private String currentKey;
+ private List currentValues;
+ private YamlFrontMatterBlock block;
+
+ public YamlFrontMatterBlockParser() {
+ inYAMLBlock = true;
+ inLiteral = false;
+ currentKey = null;
+ currentValues = new ArrayList<>();
+ block = new YamlFrontMatterBlock();
+ }
+
+ @Override
+ public Block getBlock() {
+ return block;
+ }
+
+ @Override
+ public void addLine(CharSequence line) {
+ }
+
+ @Override
+ public BlockContinue tryContinue(ParserState parserState) {
+ final CharSequence line = parserState.getLine();
+
+ if (inYAMLBlock) {
+ if (REGEX_END.matcher(line).matches()) {
+ if (currentKey != null) {
+ block.appendChild(new YamlFrontMatterNode(currentKey, currentValues));
+ }
+ return BlockContinue.finished();
+ }
+
+ Matcher matcher = REGEX_METADATA.matcher(line);
+ if (matcher.matches()) {
+ if (currentKey != null) {
+ block.appendChild(new YamlFrontMatterNode(currentKey, currentValues));
+ }
+
+ inLiteral = false;
+ currentKey = matcher.group(1);
+ currentValues = new ArrayList<>();
+ if ("|".equals(matcher.group(2))) {
+ inLiteral = true;
+ } else if (!"".equals(matcher.group(2))) {
+ currentValues.add(matcher.group(2));
+ }
+
+ return BlockContinue.atIndex(parserState.getIndex());
+ } else {
+ if (inLiteral) {
+ matcher = REGEX_METADATA_LITERAL.matcher(line);
+ if (matcher.matches()) {
+ if (currentValues.size() == 1) {
+ currentValues.set(0, currentValues.get(0) + "\n" + matcher.group(1).trim());
+ } else {
+ currentValues.add(matcher.group(1).trim());
+ }
+ }
+ } else {
+ matcher = REGEX_METADATA_LIST.matcher(line);
+ if (matcher.matches()) {
+ currentValues.add(matcher.group(1));
+ }
+ }
+
+ return BlockContinue.atIndex(parserState.getIndex());
+ }
+ } else if (REGEX_BEGIN.matcher(line).matches()) {
+ inYAMLBlock = true;
+ return BlockContinue.atIndex(parserState.getIndex());
+ }
+
+ return BlockContinue.none();
+ }
+
+ @Override
+ public void parseInlines(InlineParser inlineParser) {
+ }
+
+ public static class Factory extends AbstractBlockParserFactory {
+ @Override
+ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
+ CharSequence line = state.getLine();
+ BlockParser parentParser = matchedBlockParser.getMatchedBlockParser();
+ // check whether this line is the first line of whole document or not
+ if (parentParser instanceof DocumentBlockParser && parentParser.getBlock().getFirstChild() == null &&
+ REGEX_BEGIN.matcher(line).matches()) {
+ return BlockStart.of(new YamlFrontMatterBlockParser()).atIndex(state.getNextNonSpaceIndex());
+ }
+
+ return BlockStart.none();
+ }
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockRenderer.java b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockRenderer.java
new file mode 100644
index 000000000..436401194
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/java/org/commonmark/ext/front/matter/internal/YamlFrontMatterBlockRenderer.java
@@ -0,0 +1,15 @@
+package org.commonmark.ext.front.matter.internal;
+
+import org.commonmark.ext.front.matter.YamlFrontMatterBlock;
+import org.commonmark.ext.front.matter.YamlFrontMatterNode;
+import org.commonmark.html.CustomHtmlRenderer;
+import org.commonmark.html.HtmlWriter;
+import org.commonmark.node.Node;
+import org.commonmark.node.Visitor;
+
+public class YamlFrontMatterBlockRenderer implements CustomHtmlRenderer {
+ @Override
+ public boolean render(Node node, HtmlWriter htmlWriter, Visitor visitor) {
+ return node instanceof YamlFrontMatterBlock || node instanceof YamlFrontMatterNode;
+ }
+}
diff --git a/commonmark-ext-yaml-front-matter/src/main/javadoc/overview.html b/commonmark-ext-yaml-front-matter/src/main/javadoc/overview.html
new file mode 100644
index 000000000..d5ef92fdc
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/main/javadoc/overview.html
@@ -0,0 +1,6 @@
+
+
+Extension for YAML front matter
+See {@link org.commonmark.ext.yaml.YamlFrontMatterExtension}
+
+
diff --git a/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java b/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java
new file mode 100644
index 000000000..619d44be8
--- /dev/null
+++ b/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java
@@ -0,0 +1,223 @@
+package org.commonmark.ext.front.matter;
+
+import org.commonmark.Extension;
+import org.commonmark.html.HtmlRenderer;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.commonmark.test.RenderingTestCase;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class YamlFrontMatterTest extends RenderingTestCase {
+ private static final Set EXTENSIONS = Collections.singleton(YamlFrontMatterExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void simpleValue() {
+ final String input = "---" +
+ "\nhello: world" +
+ "\n..." +
+ "\n" +
+ "\ngreat";
+ final String rendered = "great
\n";
+
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ Node document = PARSER.parse(input);
+ document.accept(visitor);
+
+ Map> data = visitor.getData();
+
+ assertEquals(1, data.size());
+ assertEquals("hello", data.keySet().iterator().next());
+ assertEquals(1, data.get("hello").size());
+ assertEquals("world", data.get("hello").get(0));
+
+ assertRendering(input, rendered);
+ }
+
+ @Test
+ public void emptyValue() {
+ final String input = "---" +
+ "\nkey:" +
+ "\n---" +
+ "\n" +
+ "\ngreat";
+ final String rendered = "great
\n";
+
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ Node document = PARSER.parse(input);
+ document.accept(visitor);
+
+ Map> data = visitor.getData();
+
+ assertEquals(1, data.size());
+ assertEquals("key", data.keySet().iterator().next());
+ assertEquals(0, data.get("key").size());
+
+ assertRendering(input, rendered);
+ }
+
+ @Test
+ public void listValues() {
+ final String input = "---" +
+ "\nlist:" +
+ "\n - value1" +
+ "\n - value2" +
+ "\n..." +
+ "\n" +
+ "\ngreat";
+ final String rendered = "great
\n";
+
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ Node document = PARSER.parse(input);
+ document.accept(visitor);
+
+ Map> data = visitor.getData();
+
+ assertEquals(1, data.size());
+ assertTrue(data.containsKey("list"));
+ assertEquals(2, data.get("list").size());
+ assertEquals("value1", data.get("list").get(0));
+ assertEquals("value2", data.get("list").get(1));
+
+ assertRendering(input, rendered);
+ }
+
+ @Test
+ public void literalValue1() {
+ final String input = "---" +
+ "\nliteral: |" +
+ "\n hello markdown!" +
+ "\n literal thing..." +
+ "\n---" +
+ "\n" +
+ "\ngreat";
+ final String rendered = "great
\n";
+
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ Node document = PARSER.parse(input);
+ document.accept(visitor);
+
+ Map> data = visitor.getData();
+
+ assertEquals(1, data.size());
+ assertTrue(data.containsKey("literal"));
+ assertEquals(1, data.get("literal").size());
+ assertEquals("hello markdown!\nliteral thing...", data.get("literal").get(0));
+
+ assertRendering(input, rendered);
+ }
+
+ @Test
+ public void literalValue2() {
+ final String input = "---" +
+ "\nliteral: |" +
+ "\n - hello markdown!" +
+ "\n---" +
+ "\n" +
+ "\ngreat";
+ final String rendered = "great
\n";
+
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ Node document = PARSER.parse(input);
+ document.accept(visitor);
+
+ Map> data = visitor.getData();
+
+ assertEquals(1, data.size());
+ assertTrue(data.containsKey("literal"));
+ assertEquals(1, data.get("literal").size());
+ assertEquals("- hello markdown!", data.get("literal").get(0));
+
+ assertRendering(input, rendered);
+ }
+
+ @Test
+ public void complexValues() {
+ final String input = "---" +
+ "\nsimple: value" +
+ "\nliteral: |" +
+ "\n hello markdown!" +
+ "\n" +
+ "\n literal literal" +
+ "\nlist:" +
+ "\n - value1" +
+ "\n - value2" +
+ "\n---" +
+ "\ngreat";
+ final String rendered = "great
\n";
+
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ Node document = PARSER.parse(input);
+ document.accept(visitor);
+
+ Map> data = visitor.getData();
+
+ assertEquals(3, data.size());
+
+ assertTrue(data.containsKey("simple"));
+ assertEquals(1, data.get("simple").size());
+ assertEquals("value", data.get("simple").get(0));
+
+ assertTrue(data.containsKey("literal"));
+ assertEquals(1, data.get("literal").size());
+ assertEquals("hello markdown!\n\nliteral literal", data.get("literal").get(0));
+
+ assertTrue(data.containsKey("list"));
+ assertEquals(2, data.get("list").size());
+ assertEquals("value1", data.get("list").get(0));
+ assertEquals("value2", data.get("list").get(1));
+
+ assertRendering(input, rendered);
+ }
+
+ @Test
+ public void yamlInParagraph() {
+ final String input = "# hello\n" +
+ "\nhello markdown world!" +
+ "\n---" +
+ "\nhello: world" +
+ "\n---";
+ final String rendered = "hello
\nhello markdown world!
\nhello: world
\n";
+
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ Node document = PARSER.parse(input);
+ document.accept(visitor);
+
+ Map> data = visitor.getData();
+
+ assertTrue(data.isEmpty());
+
+ assertRendering(input, rendered);
+ }
+
+ @Test
+ public void nonMatchedStartTag() {
+ final String input = "----\n" +
+ "test";
+ final String rendered = "
\ntest
\n";
+
+ YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
+ Node document = PARSER.parse(input);
+ document.accept(visitor);
+
+ Map> data = visitor.getData();
+
+ assertTrue(data.isEmpty());
+
+ assertRendering(input, rendered);
+ }
+
+ @Override
+ protected String render(String source) {
+ return RENDERER.render(PARSER.parse(source));
+ }
+}
diff --git a/commonmark-integration-test/pom.xml b/commonmark-integration-test/pom.xml
index 784a2d026..7e985e7b1 100644
--- a/commonmark-integration-test/pom.xml
+++ b/commonmark-integration-test/pom.xml
@@ -32,6 +32,11 @@
commonmark-ext-gfm-tables
test
+
+ com.atlassian.commonmark
+ commonmark-ext-yaml-front-matter
+ test
+
org.pegdown
pegdown
diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java
index be5f9d890..a45e0375d 100644
--- a/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java
+++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java
@@ -4,6 +4,7 @@
import org.commonmark.ext.autolink.AutolinkExtension;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.ext.gfm.tables.TablesExtension;
+import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
import org.commonmark.html.HtmlRenderer;
import org.commonmark.parser.Parser;
import org.commonmark.spec.SpecExample;
@@ -20,7 +21,8 @@ public class SpecIntegrationTest extends SpecTestCase {
private static final List EXTENSIONS = Arrays.asList(
AutolinkExtension.create(),
StrikethroughExtension.create(),
- TablesExtension.create());
+ TablesExtension.create(),
+ YamlFrontMatterExtension.create());
private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
// The spec says URL-escaping is optional, but the examples assume that it's enabled.
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).percentEncodeUrls(true).build();
@@ -67,6 +69,10 @@ private static Map getOverriddenExamples() {
// Plain autolink
m.put("foo@bar.example.com\n", "foo@bar.example.com
\n");
+ // YAML front matter block
+ m.put("---\nFoo\n---\nBar\n---\nBaz\n", "Bar
\nBaz
\n");
+ m.put("---\n---\n", "");
+
return m;
}
diff --git a/pom.xml b/pom.xml
index 1d5ee2c14..de78615b7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,6 +24,7 @@
commonmark-ext-autolink
commonmark-ext-gfm-strikethrough
commonmark-ext-gfm-tables
+ commonmark-ext-yaml-front-matter
commonmark-integration-test
commonmark-test-util
@@ -100,6 +101,11 @@
commonmark-ext-gfm-tables
0.4.2-SNAPSHOT
+
+ com.atlassian.commonmark
+ commonmark-ext-yaml-front-matter
+ 0.4.2-SNAPSHOT
+
com.atlassian.commonmark
commonmark-test-util