-
Notifications
You must be signed in to change notification settings - Fork 290
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #332 from commonmark/footnotes-extension
Footnotes extension
- Loading branch information
Showing
45 changed files
with
2,521 additions
and
211 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>org.commonmark</groupId> | ||
<artifactId>commonmark-parent</artifactId> | ||
<version>0.22.1-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>commonmark-ext-footnotes</artifactId> | ||
<name>commonmark-java extension for footnotes</name> | ||
<description>commonmark-java extension for footnotes using [^1] syntax</description> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.commonmark</groupId> | ||
<artifactId>commonmark</artifactId> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.commonmark</groupId> | ||
<artifactId>commonmark-test-util</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
</project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module org.commonmark.ext.footnotes { | ||
exports org.commonmark.ext.footnotes; | ||
|
||
requires org.commonmark; | ||
} |
27 changes: 27 additions & 0 deletions
27
commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnoteDefinition.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package org.commonmark.ext.footnotes; | ||
|
||
import org.commonmark.node.CustomBlock; | ||
|
||
/** | ||
* A footnote definition, e.g.: | ||
* <pre><code> | ||
* [^foo]: This is the footnote text | ||
* </code></pre> | ||
* The {@link #getLabel() label} is the text in brackets after {@code ^}, so {@code foo} in the example. The contents | ||
* of the footnote are child nodes of the definition, a {@link org.commonmark.node.Paragraph} in the example. | ||
* <p> | ||
* Footnote definitions are parsed even if there's no corresponding {@link FootnoteReference}. | ||
*/ | ||
public class FootnoteDefinition extends CustomBlock { | ||
|
||
private String label; | ||
|
||
public FootnoteDefinition(String label) { | ||
this.label = label; | ||
} | ||
|
||
public String getLabel() { | ||
return label; | ||
} | ||
} | ||
|
21 changes: 21 additions & 0 deletions
21
commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnoteReference.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package org.commonmark.ext.footnotes; | ||
|
||
import org.commonmark.node.CustomNode; | ||
|
||
/** | ||
* A footnote reference, e.g. <code>[^foo]</code> in <code>Some text with a footnote[^foo]</code> | ||
* <p> | ||
* The {@link #getLabel() label} is the text within brackets after {@code ^}, so {@code foo} in the example. It needs to | ||
* match the label of a corresponding {@link FootnoteDefinition} for the footnote to be parsed. | ||
*/ | ||
public class FootnoteReference extends CustomNode { | ||
private String label; | ||
|
||
public FootnoteReference(String label) { | ||
this.label = label; | ||
} | ||
|
||
public String getLabel() { | ||
return label; | ||
} | ||
} |
106 changes: 106 additions & 0 deletions
106
commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnotesExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package org.commonmark.ext.footnotes; | ||
|
||
import org.commonmark.Extension; | ||
import org.commonmark.ext.footnotes.internal.*; | ||
import org.commonmark.parser.Parser; | ||
import org.commonmark.parser.beta.InlineContentParserFactory; | ||
import org.commonmark.renderer.NodeRenderer; | ||
import org.commonmark.renderer.html.HtmlRenderer; | ||
import org.commonmark.renderer.markdown.MarkdownNodeRendererContext; | ||
import org.commonmark.renderer.markdown.MarkdownNodeRendererFactory; | ||
import org.commonmark.renderer.markdown.MarkdownRenderer; | ||
|
||
import java.util.Set; | ||
|
||
/** | ||
* Extension for footnotes with syntax like GitHub Flavored Markdown: | ||
* <pre><code> | ||
* Some text with a footnote[^1]. | ||
* | ||
* [^1]: The text of the footnote. | ||
* </code></pre> | ||
* The <code>[^1]</code> is a {@link FootnoteReference}, with "1" being the label. | ||
* <p> | ||
* The line with <code>[^1]: ...</code> is a {@link FootnoteDefinition}, with the contents as child nodes (can be a | ||
* paragraph like in the example, or other blocks like lists). | ||
* <p> | ||
* All the footnotes (definitions) will be rendered in a list at the end of a document, no matter where they appear in | ||
* the source. The footnotes will be numbered starting from 1, then 2, etc, depending on the order in which they appear | ||
* in the text (and not dependent on the label). The footnote reference is a link to the footnote, and from the footnote | ||
* there is a link back to the reference (or multiple). | ||
* <p> | ||
* There is also optional support for inline footnotes, use {@link #builder()} and then set {@link Builder#inlineFootnotes}. | ||
* | ||
* @see <a href="https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#footnotes">GitHub docs for footnotes</a> | ||
*/ | ||
public class FootnotesExtension implements Parser.ParserExtension, | ||
HtmlRenderer.HtmlRendererExtension, | ||
MarkdownRenderer.MarkdownRendererExtension { | ||
|
||
private final boolean inlineFootnotes; | ||
|
||
private FootnotesExtension(boolean inlineFootnotes) { | ||
this.inlineFootnotes = inlineFootnotes; | ||
} | ||
|
||
/** | ||
* The extension with the default configuration (no support for inline footnotes). | ||
*/ | ||
public static Extension create() { | ||
return builder().build(); | ||
} | ||
|
||
public static Builder builder() { | ||
return new Builder(); | ||
} | ||
|
||
@Override | ||
public void extend(Parser.Builder parserBuilder) { | ||
parserBuilder | ||
.customBlockParserFactory(new FootnoteBlockParser.Factory()) | ||
.linkProcessor(new FootnoteLinkProcessor()); | ||
if (inlineFootnotes) { | ||
parserBuilder.linkMarker('^'); | ||
} | ||
} | ||
|
||
@Override | ||
public void extend(HtmlRenderer.Builder rendererBuilder) { | ||
rendererBuilder.nodeRendererFactory(FootnoteHtmlNodeRenderer::new); | ||
} | ||
|
||
@Override | ||
public void extend(MarkdownRenderer.Builder rendererBuilder) { | ||
rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() { | ||
@Override | ||
public NodeRenderer create(MarkdownNodeRendererContext context) { | ||
return new FootnoteMarkdownNodeRenderer(context); | ||
} | ||
|
||
@Override | ||
public Set<Character> getSpecialCharacters() { | ||
return Set.of(); | ||
} | ||
}); | ||
} | ||
|
||
public static class Builder { | ||
|
||
private boolean inlineFootnotes = false; | ||
|
||
/** | ||
* Enable support for inline footnotes without definitions, e.g.: | ||
* <pre> | ||
* Some text^[this is an inline footnote] | ||
* </pre> | ||
*/ | ||
public Builder inlineFootnotes(boolean inlineFootnotes) { | ||
this.inlineFootnotes = inlineFootnotes; | ||
return this; | ||
} | ||
|
||
public FootnotesExtension build() { | ||
return new FootnotesExtension(inlineFootnotes); | ||
} | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/InlineFootnote.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package org.commonmark.ext.footnotes; | ||
|
||
import org.commonmark.node.CustomNode; | ||
|
||
public class InlineFootnote extends CustomNode { | ||
} |
101 changes: 101 additions & 0 deletions
101
...xt-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteBlockParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
package org.commonmark.ext.footnotes.internal; | ||
|
||
import org.commonmark.ext.footnotes.FootnoteDefinition; | ||
import org.commonmark.node.Block; | ||
import org.commonmark.node.DefinitionMap; | ||
import org.commonmark.parser.block.*; | ||
import org.commonmark.text.Characters; | ||
|
||
import java.util.List; | ||
|
||
/** | ||
* Parser for a single {@link FootnoteDefinition} block. | ||
*/ | ||
public class FootnoteBlockParser extends AbstractBlockParser { | ||
|
||
private final FootnoteDefinition block; | ||
|
||
public FootnoteBlockParser(String label) { | ||
block = new FootnoteDefinition(label); | ||
} | ||
|
||
@Override | ||
public Block getBlock() { | ||
return block; | ||
} | ||
|
||
@Override | ||
public boolean isContainer() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean canContain(Block childBlock) { | ||
return true; | ||
} | ||
|
||
@Override | ||
public BlockContinue tryContinue(ParserState parserState) { | ||
if (parserState.getIndent() >= 4) { | ||
// It looks like content needs to be indented by 4 so that it's part of a footnote (instead of starting a new block). | ||
return BlockContinue.atColumn(4); | ||
} else { | ||
// We're not continuing to give other block parsers a chance to interrupt this definition. | ||
// But if no other block parser applied (including another FootnotesBlockParser), we will | ||
// accept the line via lazy continuation (same as a block quote). | ||
return BlockContinue.none(); | ||
} | ||
} | ||
|
||
@Override | ||
public List<DefinitionMap<?>> getDefinitions() { | ||
var map = new DefinitionMap<>(FootnoteDefinition.class); | ||
map.putIfAbsent(block.getLabel(), block); | ||
return List.of(map); | ||
} | ||
|
||
public static class Factory implements BlockParserFactory { | ||
|
||
@Override | ||
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { | ||
if (state.getIndent() >= 4) { | ||
return BlockStart.none(); | ||
} | ||
var index = state.getNextNonSpaceIndex(); | ||
var content = state.getLine().getContent(); | ||
if (content.charAt(index) != '[' || index + 1 >= content.length()) { | ||
return BlockStart.none(); | ||
} | ||
index++; | ||
if (content.charAt(index) != '^' || index + 1 >= content.length()) { | ||
return BlockStart.none(); | ||
} | ||
// Now at first label character (if any) | ||
index++; | ||
var labelStart = index; | ||
|
||
for (index = labelStart; index < content.length(); index++) { | ||
var c = content.charAt(index); | ||
switch (c) { | ||
case ']': | ||
if (index > labelStart && index + 1 < content.length() && content.charAt(index + 1) == ':') { | ||
var label = content.subSequence(labelStart, index).toString(); | ||
// After the colon, any number of spaces is skipped (not part of the content) | ||
var afterSpaces = Characters.skipSpaceTab(content, index + 2, content.length()); | ||
return BlockStart.of(new FootnoteBlockParser(label)).atIndex(afterSpaces); | ||
} else { | ||
return BlockStart.none(); | ||
} | ||
case ' ': | ||
case '\r': | ||
case '\n': | ||
case '\0': | ||
case '\t': | ||
return BlockStart.none(); | ||
} | ||
} | ||
|
||
return BlockStart.none(); | ||
} | ||
} | ||
} |
Oops, something went wrong.