diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 381d7c73f..b32794271 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,46 +9,53 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- java: [1.8, 11, 17]
+ java: [11, 17, 21, 25]
steps:
- name: Checkout sources
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up JDK
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
+ distribution: 'zulu'
- name: Build
run: mvn -B package javadoc:javadoc
coverage:
runs-on: ubuntu-latest
+ if: ${{ github.event_name == 'push' }}
steps:
- name: Checkout sources
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up JDK
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v4
with:
- java-version: 1.8
+ java-version: 11
+ distribution: 'zulu'
- name: Build with coverage
run: mvn -B -Pcoverage clean test jacoco:report-aggregate
- name: Publish coverage
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v4
+ with:
+ fail_ci_if_error: true
+ token: ${{ secrets.CODECOV_TOKEN }}
android-compatibility:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up JDK
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v4
with:
- java-version: 1.8
+ java-version: 11
+ distribution: 'zulu'
- name: Android Lint checks
run: cd commonmark-android-test && ./gradlew :app:lint
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4edf451c0..c0531ca55 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,15 +14,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- name: Set up Maven Central repository
- uses: actions/setup-java@v1
+ uses: actions/setup-java@v4
with:
- java-version: 1.8
- server-id: ossrh
- server-username: MAVEN_USERNAME # env variable to use for username in release
- server-password: MAVEN_PASSWORD # env variable to use for password in release
+ java-version: 24
+ distribution: 'zulu'
+ # See https://central.sonatype.org/publish/publish-portal-maven/
+ server-id: central
+ server-username: CENTRAL_USERNAME # env variable to use for username in release
+ server-password: CENTRAL_PASSWORD # env variable to use for password in release
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable to use for passphrase in release
@@ -36,6 +38,6 @@ jobs:
mvn -B -Dusername=${{ secrets.GH_USERNAME }} -Dpassword=${{ secrets.GH_ACCESS_TOKEN }} release:prepare
mvn -B release:perform
env:
- MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
- MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
+ CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
+ CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
diff --git a/.gitignore b/.gitignore
index a156931f0..d998d8890 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,6 @@
# Maven
target/
+
+# macOS
+.DS_Store
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 000000000..4d245050f
--- /dev/null
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cdfb65043..9c5c67268 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,182 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html),
with the exception that 0.x versions can break between minor versions.
+## [Unreleased]
+### Added
+- Allow customizing HTML attributes for alert title `
+ * 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.
+ *
+ * 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;
+ }
+}
+
diff --git a/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnoteReference.java b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnoteReference.java
new file mode 100644
index 000000000..61dcf8626
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnoteReference.java
@@ -0,0 +1,21 @@
+package org.commonmark.ext.footnotes;
+
+import org.commonmark.node.CustomNode;
+
+/**
+ * A footnote reference, e.g. [^foo] in Some text with a footnote[^foo]
+ *
+ * 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;
+ }
+}
diff --git a/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnotesExtension.java b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnotesExtension.java
new file mode 100644
index 000000000..dd532fa34
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/FootnotesExtension.java
@@ -0,0 +1,105 @@
+package org.commonmark.ext.footnotes;
+
+import org.commonmark.Extension;
+import org.commonmark.ext.footnotes.internal.*;
+import org.commonmark.parser.Parser;
+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:
+ *
+ * Some text with a footnote[^1].
+ *
+ * [^1]: The text of the footnote.
+ *
+ * The [^1] is a {@link FootnoteReference}, with "1" being the label.
+ *
+ * The line with [^1]: ... is a {@link FootnoteDefinition}, with the contents as child nodes (can be a
+ * paragraph like in the example, or other blocks like lists).
+ *
+ * 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).
+ *
+ * There is also optional support for inline footnotes, use {@link #builder()} and then set {@link Builder#inlineFootnotes}.
+ *
+ * @see GitHub docs for footnotes
+ */
+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 getSpecialCharacters() {
+ return Set.of();
+ }
+ });
+ }
+
+ public static class Builder {
+
+ private boolean inlineFootnotes = false;
+
+ /**
+ * Enable support for inline footnotes without definitions, e.g.:
+ *
+ * Some text^[this is an inline footnote]
+ *
+ */
+ public Builder inlineFootnotes(boolean inlineFootnotes) {
+ this.inlineFootnotes = inlineFootnotes;
+ return this;
+ }
+
+ public FootnotesExtension build() {
+ return new FootnotesExtension(inlineFootnotes);
+ }
+ }
+}
diff --git a/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/InlineFootnote.java b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/InlineFootnote.java
new file mode 100644
index 000000000..665d01936
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/InlineFootnote.java
@@ -0,0 +1,6 @@
+package org.commonmark.ext.footnotes;
+
+import org.commonmark.node.CustomNode;
+
+public class InlineFootnote extends CustomNode {
+}
diff --git a/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteBlockParser.java b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteBlockParser.java
new file mode 100644
index 000000000..110bdef20
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteBlockParser.java
@@ -0,0 +1,105 @@
+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 if (parserState.isBlank()) {
+ // A blank line doesn't finish a footnote yet. If there's another line with indent >= 4 after it,
+ // that should result in another paragraph in this footnote definition.
+ return BlockContinue.atIndex(parserState.getIndex());
+ } 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> 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();
+ }
+ }
+}
diff --git a/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteHtmlNodeRenderer.java b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteHtmlNodeRenderer.java
new file mode 100644
index 000000000..70eb048a3
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteHtmlNodeRenderer.java
@@ -0,0 +1,391 @@
+package org.commonmark.ext.footnotes.internal;
+
+import org.commonmark.ext.footnotes.FootnoteDefinition;
+import org.commonmark.ext.footnotes.FootnoteReference;
+import org.commonmark.ext.footnotes.InlineFootnote;
+import org.commonmark.node.*;
+import org.commonmark.renderer.NodeRenderer;
+import org.commonmark.renderer.html.HtmlNodeRendererContext;
+import org.commonmark.renderer.html.HtmlWriter;
+
+import java.util.*;
+import java.util.function.Consumer;
+
+/**
+ * HTML rendering for footnotes.
+ *
+ * Aims to match the rendering of cmark-gfm (which is slightly different from GitHub's when it comes to class
+ * attributes, not sure why).
+ *
+ * Some notes on how rendering works:
+ *
+ *
Footnotes are numbered according to the order of references, starting at 1
+ *
Definitions are rendered at the end of the document, regardless of where the definition was in the source
+ *
Definitions are ordered by number
+ *
Definitions have links back to their references (one or more)
+ *
+ *
+ *
Nested footnotes
+ * Text in footnote definitions can reference other footnotes, even ones that aren't referenced in the main text.
+ * This makes them tricky because it's not enough to just go through the main text for references.
+ * And before we can render a definition, we need to know all references (because we add links back to references).
+ *
+ * In other words, footnotes form a directed graph. Footnotes can reference each other so cycles are possible too.
+ *
+ * One way to implement it, which is what cmark-gfm does, is to go through the whole document (including definitions)
+ * and find all references in order. That guarantees that all definitions are found, but it has strange results for
+ * ordering or when the reference is in an unreferenced definition, see tests. In graph terms, it renders all
+ * definitions that have an incoming edge, no matter whether they are connected to the main text or not.
+ *
+ * The way we implement it:
+ *
+ *
Start with the references in the main text; we can render them as we go
+ *
After the main text is rendered, we have the referenced definitions, but there might be more from definition text
+ *
To find the remaining definitions, we visit the definitions from before to look at references
+ *
Repeat (breadth-first search) until we've found all definitions (note that we can't render before that's done because of backrefs)
+ *
Now render the definitions (and any references inside)
+ *
+ * This means we only render definitions whose references are actually rendered, and in a meaningful order (all main
+ * text footnotes first, then any nested ones).
+ */
+public class FootnoteHtmlNodeRenderer implements NodeRenderer {
+
+ private final HtmlWriter html;
+ private final HtmlNodeRendererContext context;
+
+ /**
+ * All definitions (even potentially unused ones), for looking up references
+ */
+ private DefinitionMap definitionMap;
+
+ /**
+ * Definitions that were referenced, in order in which they should be rendered.
+ */
+ private final Map referencedDefinitions = new LinkedHashMap<>();
+
+ /**
+ * Information about references that should be rendered as footnotes. This doesn't contain all references, just the
+ * ones from inside definitions.
+ */
+ private final Map references = new HashMap<>();
+
+ public FootnoteHtmlNodeRenderer(HtmlNodeRendererContext context) {
+ this.html = context.getWriter();
+ this.context = context;
+ }
+
+ @Override
+ public Set> getNodeTypes() {
+ return Set.of(FootnoteReference.class, InlineFootnote.class, FootnoteDefinition.class);
+ }
+
+ @Override
+ public void beforeRoot(Node rootNode) {
+ // Collect all definitions first, so we can look them up when encountering a reference later.
+ var visitor = new DefinitionVisitor();
+ rootNode.accept(visitor);
+ definitionMap = visitor.definitions;
+ }
+
+ @Override
+ public void render(Node node) {
+ if (node instanceof FootnoteReference) {
+ // This is called for all references, even ones inside definitions that we render at the end.
+ // Inside definitions, we have registered the reference already.
+ var ref = (FootnoteReference) node;
+ // Use containsKey because if value is null, we don't need to try registering again.
+ var info = references.containsKey(ref) ? references.get(ref) : tryRegisterReference(ref);
+ if (info != null) {
+ renderReference(ref, info);
+ } else {
+ // A reference without a corresponding definition is rendered as plain text
+ html.text("[^" + ref.getLabel() + "]");
+ }
+ } else if (node instanceof InlineFootnote) {
+ var info = references.get(node);
+ if (info == null) {
+ info = registerReference(node, null);
+ }
+ renderReference(node, info);
+ }
+ }
+
+ @Override
+ public void afterRoot(Node rootNode) {
+ // Now render the referenced definitions if there are any.
+ if (referencedDefinitions.isEmpty()) {
+ return;
+ }
+
+ var firstDef = referencedDefinitions.keySet().iterator().next();
+ var attrs = new LinkedHashMap();
+ attrs.put("class", "footnotes");
+ attrs.put("data-footnotes", null);
+ html.tag("section", context.extendAttributes(firstDef, "section", attrs));
+ html.line();
+ html.tag("ol");
+ html.line();
+
+ // Check whether there are any footnotes inside the definitions that we're about to render. For those, we might
+ // need to render more definitions. So do a breadth-first search to find all relevant definitions.
+ var check = new LinkedList<>(referencedDefinitions.keySet());
+ while (!check.isEmpty()) {
+ var def = check.removeFirst();
+ def.accept(new ShallowReferenceVisitor(def, node -> {
+ if (node instanceof FootnoteReference) {
+ var ref = (FootnoteReference) node;
+ var d = definitionMap.get(ref.getLabel());
+ if (d != null) {
+ if (!referencedDefinitions.containsKey(d)) {
+ check.addLast(d);
+ }
+ references.put(ref, registerReference(d, d.getLabel()));
+ }
+ } else if (node instanceof InlineFootnote) {
+ check.addLast(node);
+ references.put(node, registerReference(node, null));
+ }
+ }));
+ }
+
+ for (var entry : referencedDefinitions.entrySet()) {
+ // This will also render any footnote references inside definitions
+ renderDefinition(entry.getKey(), entry.getValue());
+ }
+
+ html.tag("/ol");
+ html.line();
+ html.tag("/section");
+ html.line();
+ }
+
+ private ReferenceInfo tryRegisterReference(FootnoteReference ref) {
+ var def = definitionMap.get(ref.getLabel());
+ if (def == null) {
+ return null;
+ }
+ return registerReference(def, def.getLabel());
+ }
+
+ private ReferenceInfo registerReference(Node node, String label) {
+ // The first referenced definition gets number 1, second one 2, etc.
+ var referencedDef = referencedDefinitions.computeIfAbsent(node, k -> {
+ var num = referencedDefinitions.size() + 1;
+ var key = definitionKey(label, num);
+ return new ReferencedDefinition(num, key);
+ });
+ var definitionNumber = referencedDef.definitionNumber;
+ // The reference number for that particular definition. E.g. if there's two references for the same definition,
+ // the first one is 1, the second one 2, etc. This is needed to give each reference a unique ID so that each
+ // reference can get its own backlink from the definition.
+ var refNumber = referencedDef.references.size() + 1;
+ var definitionKey = referencedDef.definitionKey;
+ var id = referenceId(definitionKey, refNumber);
+ referencedDef.references.add(id);
+
+ return new ReferenceInfo(id, definitionId(definitionKey), definitionNumber);
+ }
+
+ private void renderReference(Node node, ReferenceInfo referenceInfo) {
+ html.tag("sup", context.extendAttributes(node, "sup", Map.of("class", "footnote-ref")));
+
+ var href = "#" + referenceInfo.definitionId;
+ var attrs = new LinkedHashMap();
+ attrs.put("href", href);
+ attrs.put("id", referenceInfo.id);
+ attrs.put("data-footnote-ref", null);
+ html.tag("a", context.extendAttributes(node, "a", attrs));
+ html.raw(String.valueOf(referenceInfo.definitionNumber));
+ html.tag("/a");
+ html.tag("/sup");
+ }
+
+ private void renderDefinition(Node def, ReferencedDefinition referencedDefinition) {
+ var attrs = new LinkedHashMap();
+ attrs.put("id", definitionId(referencedDefinition.definitionKey));
+ html.tag("li", context.extendAttributes(def, "li", attrs));
+ html.line();
+
+ if (def.getLastChild() instanceof Paragraph) {
+ // Add backlinks into last paragraph before
. This is what GFM does.
+ var lastParagraph = (Paragraph) def.getLastChild();
+ var node = def.getFirstChild();
+ while (node != lastParagraph) {
+ if (node instanceof Paragraph) {
+ // Because we're manually rendering the
for the last paragraph, do the same for all other
+ // paragraphs for consistency (Paragraph rendering might be overwritten by a custom renderer).
+ html.tag("p", context.extendAttributes(node, "p", Map.of()));
+ renderChildren(node);
+ html.tag("/p");
+ html.line();
+ } else {
+ context.render(node);
+ }
+ node = node.getNext();
+ }
+
+ html.tag("p", context.extendAttributes(lastParagraph, "p", Map.of()));
+ renderChildren(lastParagraph);
+ html.raw(" ");
+ renderBackrefs(def, referencedDefinition);
+ html.tag("/p");
+ html.line();
+ } else if (def instanceof InlineFootnote) {
+ html.tag("p", context.extendAttributes(def, "p", Map.of()));
+ renderChildren(def);
+ html.raw(" ");
+ renderBackrefs(def, referencedDefinition);
+ html.tag("/p");
+ html.line();
+ } else {
+ renderChildren(def);
+ html.line();
+ renderBackrefs(def, referencedDefinition);
+ }
+
+ html.tag("/li");
+ html.line();
+ }
+
+ private void renderBackrefs(Node def, ReferencedDefinition referencedDefinition) {
+ var refs = referencedDefinition.references;
+ for (int i = 0; i < refs.size(); i++) {
+ var ref = refs.get(i);
+ var refNumber = i + 1;
+ var idx = referencedDefinition.definitionNumber + (refNumber > 1 ? ("-" + refNumber) : "");
+
+ var attrs = new LinkedHashMap();
+ attrs.put("href", "#" + ref);
+ attrs.put("class", "footnote-backref");
+ attrs.put("data-footnote-backref", null);
+ attrs.put("data-footnote-backref-idx", idx);
+ attrs.put("aria-label", "Back to reference " + idx);
+ html.tag("a", context.extendAttributes(def, "a", attrs));
+ if (refNumber > 1) {
+ html.tag("sup", context.extendAttributes(def, "sup", Map.of("class", "footnote-ref")));
+ html.raw(String.valueOf(refNumber));
+ html.tag("/sup");
+ }
+ // U+21A9 LEFTWARDS ARROW WITH HOOK
+ html.raw("\u21A9");
+ html.tag("/a");
+ if (i + 1 < refs.size()) {
+ html.raw(" ");
+ }
+ }
+ }
+
+ private String referenceId(String definitionKey, int number) {
+ return "fnref" + definitionKey + (number == 1 ? "" : ("-" + number));
+ }
+
+ private String definitionKey(String label, int number) {
+ // Named definitions use the pattern "fn-{name}" and inline definitions use "fn{number}" so as not to conflict.
+ // "fn{number}" is also what pandoc uses (for all types), starting with number 1.
+ if (label != null) {
+ return "-" + label;
+ } else {
+ return "" + number;
+ }
+ }
+
+ private String definitionId(String definitionKey) {
+ return "fn" + definitionKey;
+ }
+
+ private void renderChildren(Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ Node next = node.getNext();
+ context.render(node);
+ node = next;
+ }
+ }
+
+ private static class DefinitionVisitor extends AbstractVisitor {
+
+ private final DefinitionMap definitions = new DefinitionMap<>(FootnoteDefinition.class);
+
+ @Override
+ public void visit(CustomBlock customBlock) {
+ if (customBlock instanceof FootnoteDefinition) {
+ var def = (FootnoteDefinition) customBlock;
+ definitions.putIfAbsent(def.getLabel(), def);
+ } else {
+ super.visit(customBlock);
+ }
+ }
+ }
+
+ /**
+ * Visit footnote references/inline footnotes inside the parent (but not the parent itself). We want a shallow visit
+ * because the caller wants to control when to descend.
+ */
+ private static class ShallowReferenceVisitor extends AbstractVisitor {
+ private final Node parent;
+ private final Consumer consumer;
+
+ private ShallowReferenceVisitor(Node parent, Consumer consumer) {
+ this.parent = parent;
+ this.consumer = consumer;
+ }
+
+ @Override
+ public void visit(CustomNode customNode) {
+ if (customNode instanceof FootnoteReference) {
+ consumer.accept(customNode);
+ } else if (customNode instanceof InlineFootnote) {
+ if (customNode == parent) {
+ // Descend into the parent (inline footnotes can contain inline footnotes)
+ super.visit(customNode);
+ } else {
+ // Don't descend here because we want to be shallow.
+ consumer.accept(customNode);
+ }
+ } else {
+ super.visit(customNode);
+ }
+ }
+ }
+
+ private static class ReferencedDefinition {
+ /**
+ * The definition number, starting from 1, and in order in which they're referenced.
+ */
+ final int definitionNumber;
+ /**
+ * The unique key of the definition. Together with a static prefix it forms the ID used in the HTML.
+ */
+ final String definitionKey;
+ /**
+ * The IDs of references for this definition, for backrefs.
+ */
+ final List references = new ArrayList<>();
+
+ ReferencedDefinition(int definitionNumber, String definitionKey) {
+ this.definitionNumber = definitionNumber;
+ this.definitionKey = definitionKey;
+ }
+ }
+
+ private static class ReferenceInfo {
+ /**
+ * The ID of the reference; in the corresponding definition, a link back to this reference will be rendered.
+ */
+ private final String id;
+ /**
+ * The ID of the definition, for linking to the definition.
+ */
+ private final String definitionId;
+ /**
+ * The definition number, rendered in superscript.
+ */
+ private final int definitionNumber;
+
+ private ReferenceInfo(String id, String definitionId, int definitionNumber) {
+ this.id = id;
+ this.definitionId = definitionId;
+ this.definitionNumber = definitionNumber;
+ }
+ }
+}
diff --git a/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteLinkProcessor.java b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteLinkProcessor.java
new file mode 100644
index 000000000..07b008576
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteLinkProcessor.java
@@ -0,0 +1,57 @@
+package org.commonmark.ext.footnotes.internal;
+
+import org.commonmark.ext.footnotes.FootnoteDefinition;
+import org.commonmark.ext.footnotes.FootnoteReference;
+import org.commonmark.ext.footnotes.InlineFootnote;
+import org.commonmark.node.LinkReferenceDefinition;
+import org.commonmark.parser.InlineParserContext;
+import org.commonmark.parser.beta.LinkInfo;
+import org.commonmark.parser.beta.LinkProcessor;
+import org.commonmark.parser.beta.LinkResult;
+import org.commonmark.parser.beta.Scanner;
+
+/**
+ * For turning e.g. [^foo] into a {@link FootnoteReference},
+ * and ^[foo] into an {@link InlineFootnote}.
+ */
+public class FootnoteLinkProcessor implements LinkProcessor {
+ @Override
+ public LinkResult process(LinkInfo linkInfo, Scanner scanner, InlineParserContext context) {
+
+ if (linkInfo.marker() != null && linkInfo.marker().getLiteral().equals("^")) {
+ // An inline footnote like ^[footnote text]. Note that we only get the marker here if the option is enabled
+ // on the extension.
+ return LinkResult.wrapTextIn(new InlineFootnote(), linkInfo.afterTextBracket()).includeMarker();
+ }
+
+ if (linkInfo.destination() != null) {
+ // If it's an inline link, it can't be a footnote reference
+ return LinkResult.none();
+ }
+
+ var text = linkInfo.text();
+ if (!text.startsWith("^")) {
+ // Footnote reference needs to start with [^
+ return LinkResult.none();
+ }
+
+ if (linkInfo.label() != null && context.getDefinition(LinkReferenceDefinition.class, linkInfo.label()) != null) {
+ // If there's a label after the text and the label has a definition -> it's a link, and it should take
+ // preference, e.g. in `[^foo][bar]` if `[bar]` has a definition, `[^foo]` won't be a footnote reference.
+ return LinkResult.none();
+ }
+
+ var label = text.substring(1);
+ // Check if we have a definition, otherwise ignore (same behavior as for link reference definitions).
+ // Note that the definition parser already checked the syntax of the label, we don't need to check again.
+ var def = context.getDefinition(FootnoteDefinition.class, label);
+ if (def == null) {
+ return LinkResult.none();
+ }
+
+ // For footnotes, we only ever consume the text part of the link, not the label part (if any)
+ var position = linkInfo.afterTextBracket();
+ // If the marker is `![`, we don't want to include the `!`, so start from bracket
+ return LinkResult.replaceWith(new FootnoteReference(label), position);
+ }
+}
diff --git a/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteMarkdownNodeRenderer.java b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteMarkdownNodeRenderer.java
new file mode 100644
index 000000000..3dcf4fc83
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/java/org/commonmark/ext/footnotes/internal/FootnoteMarkdownNodeRenderer.java
@@ -0,0 +1,70 @@
+package org.commonmark.ext.footnotes.internal;
+
+import org.commonmark.ext.footnotes.FootnoteDefinition;
+import org.commonmark.ext.footnotes.FootnoteReference;
+import org.commonmark.ext.footnotes.InlineFootnote;
+import org.commonmark.node.*;
+import org.commonmark.renderer.NodeRenderer;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownWriter;
+
+import java.util.Set;
+
+public class FootnoteMarkdownNodeRenderer implements NodeRenderer {
+
+ private final MarkdownWriter writer;
+ private final MarkdownNodeRendererContext context;
+
+ public FootnoteMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
+ this.writer = context.getWriter();
+ this.context = context;
+ }
+
+ @Override
+ public Set> getNodeTypes() {
+ return Set.of(FootnoteReference.class, InlineFootnote.class, FootnoteDefinition.class);
+ }
+
+ @Override
+ public void render(Node node) {
+ if (node instanceof FootnoteReference) {
+ renderReference((FootnoteReference) node);
+ } else if (node instanceof InlineFootnote) {
+ renderInline((InlineFootnote) node);
+ } else if (node instanceof FootnoteDefinition) {
+ renderDefinition((FootnoteDefinition) node);
+ }
+ }
+
+ private void renderReference(FootnoteReference ref) {
+ writer.raw("[^");
+ // The label is parsed as-is without escaping, so we can render it back as-is
+ writer.raw(ref.getLabel());
+ writer.raw("]");
+ }
+
+ private void renderInline(InlineFootnote inlineFootnote) {
+ writer.raw("^[");
+ renderChildren(inlineFootnote);
+ writer.raw("]");
+ }
+
+ private void renderDefinition(FootnoteDefinition def) {
+ writer.raw("[^");
+ writer.raw(def.getLabel());
+ writer.raw("]: ");
+
+ writer.pushPrefix(" ");
+ renderChildren(def);
+ writer.popPrefix();
+ }
+
+ private void renderChildren(Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ Node next = node.getNext();
+ context.render(node);
+ node = next;
+ }
+ }
+}
diff --git a/commonmark-ext-footnotes/src/main/javadoc/overview.html b/commonmark-ext-footnotes/src/main/javadoc/overview.html
new file mode 100644
index 000000000..4f19d2115
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/javadoc/overview.html
@@ -0,0 +1,6 @@
+
+
+Extension for footnotes using [^1] syntax
+
See {@link org.commonmark.ext.footnotes.FootnotesExtension}
+
+
diff --git a/commonmark-ext-footnotes/src/main/resources/META-INF/LICENSE.txt b/commonmark-ext-footnotes/src/main/resources/META-INF/LICENSE.txt
new file mode 100644
index 000000000..b09e367ce
--- /dev/null
+++ b/commonmark-ext-footnotes/src/main/resources/META-INF/LICENSE.txt
@@ -0,0 +1,23 @@
+Copyright (c) 2015, Atlassian Pty Ltd
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnoteHtmlRendererTest.java b/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnoteHtmlRendererTest.java
new file mode 100644
index 000000000..bc7d4f74c
--- /dev/null
+++ b/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnoteHtmlRendererTest.java
@@ -0,0 +1,339 @@
+package org.commonmark.ext.footnotes;
+
+import org.commonmark.Extension;
+import org.commonmark.node.Document;
+import org.commonmark.node.Paragraph;
+import org.commonmark.node.Text;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.testutil.Asserts;
+import org.commonmark.testutil.RenderingTestCase;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Set;
+
+public class FootnoteHtmlRendererTest extends RenderingTestCase {
+ private static final Set EXTENSIONS = Set.of(FootnotesExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void testOne() {
+ assertRendering("Test [^foo]\n\n[^foo]: note\n",
+ "
\n" +
+ "\n" +
+ "\n");
+ }
+
+ @Test
+ public void testLabelNormalization() {
+ // Labels match via their normalized form. For the href and IDs to match, rendering needs to use the
+ // label from the definition consistently.
+ assertRendering("Test [^bar]\n\n[^BAR]: note\n",
+ "
\n" +
+ "\n" +
+ "\n");
+ }
+
+ @Test
+ public void testMultipleReferences() {
+ // Tests a few things:
+ // - Numbering is based on the reference order, not the definition order
+ // - The same number is used when a definition is referenced multiple times
+ // - Multiple backrefs are rendered
+ assertRendering("First [^foo]\n\nThen [^bar]\n\nThen [^foo] again\n\n[^bar]: b\n[^foo]: f\n",
+ "
\n" +
+ "\n" +
+ "\n");
+ }
+
+ @Test
+ public void testNestedFootnotesOrder() {
+ // GitHub has a strange result here, the definitions are in order: 1. bar, 2. foo.
+ // The reason is that the number is done based on all references in document order, including references in
+ // definitions. So [^bar] from the first line is first.
+ assertRendering("[^foo]: foo [^bar]\n" +
+ "\n" +
+ "[^foo]\n" +
+ "\n" +
+ "[^bar]: bar\n", "
\n" +
+ "\n" +
+ "\n");
+ }
+
+ @Test
+ public void testNestedFootnotesUnreferenced() {
+ // This should not result in any footnotes, as baz itself isn't referenced.
+ // But GitHub renders bar only, with a broken backref, because bar is referenced from foo.
+ assertRendering("[^foo]: foo[^bar]\n" +
+ "[^bar]: bar\n", "");
+
+ // And here only 1 is rendered.
+ assertRendering("[^1]\n" +
+ "\n" +
+ "[^1]: one\n" +
+ "[^foo]: foo[^bar]\n" +
+ "[^bar]: bar\n", "
\n" +
+ "\n" +
+ "\n");
+ }
+
+ @Test
+ public void testInlineFootnoteWithReference() {
+ // This is a bit tricky because the IDs need to be unique.
+ assertRenderingInline("Test ^[inline [^1]]\n" +
+ "\n" +
+ "[^1]: normal",
+ "
\n" +
+ "\n" +
+ "\n");
+ }
+
+
+ @Test
+ public void testRenderNodesDirectly() {
+ // Everything should work as expected when rendering from nodes directly (no parsing step).
+ var doc = new Document();
+ var p = new Paragraph();
+ p.appendChild(new Text("Test "));
+ p.appendChild(new FootnoteReference("foo"));
+ var def = new FootnoteDefinition("foo");
+ var note = new Paragraph();
+ note.appendChild(new Text("note!"));
+ def.appendChild(note);
+ doc.appendChild(p);
+ doc.appendChild(def);
+
+ var expected = "
+````````````````````````````````
+
+## Alert content
+
+Marker alone in first paragraph, blank line, then content:
+
+```````````````````````````````` example alert
+> [!NOTE]
+>
+> Content
+.
+
+
Note
+
Content
+
+````````````````````````````````
+
+Multiple paragraphs:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> First paragraph
+>
+> Second paragraph
+.
+
+
Note
+
First paragraph
+
Second paragraph
+
+````````````````````````````````
+
+Inline formatting:
+
+```````````````````````````````` example alert
+> [!TIP]
+> This is **bold** and *italic*
+.
+
+````````````````````````````````
+
+List inside alert:
+
+```````````````````````````````` example alert
+> [!IMPORTANT]
+> Items:
+> - First item
+> - Second item
+.
+
+
Important
+
Items:
+
+
First item
+
Second item
+
+
+````````````````````````````````
+
+Links inside alert:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> Check out [this link](https://example.com) for more info
+.
+
+````````````````````````````````
+
+Empty lines in middle of alert:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> First
+>
+>
+> After empty lines
+.
+
+
Note
+
First
+
After empty lines
+
+````````````````````````````````
+
+## Not an alert
+
+Text after marker on the same line:
+
+```````````````````````````````` example alert
+> [!NOTE] Some text
+.
+
+
[!NOTE] Some text
+
+````````````````````````````````
+
+Unknown type:
+
+```````````````````````````````` example alert
+> [!INVALID]
+> Some text
+.
+
+
[!INVALID]
+Some text
+
+````````````````````````````````
+
+Unconfigured custom type is not an alert:
+
+```````````````````````````````` example alert
+> [!INFO]
+> Should be blockquote
+.
+
+
[!INFO]
+Should be blockquote
+
+````````````````````````````````
+
+Marker with no content:
+
+```````````````````````````````` example alert
+> [!NOTE]
+.
+
+
[!NOTE]
+
+````````````````````````````````
+
+Whitespace-only content after marker:
+
+```````````````````````````````` example alert
+> [!TIP]
+>
+>
+.
+
+
[!TIP]
+
+````````````````````````````````
+
+Extra space inside marker:
+
+```````````````````````````````` example alert
+> [! NOTE]
+> Should be blockquote
+.
+
+
[! NOTE]
+Should be blockquote
+
+````````````````````````````````
+
+Missing brackets:
+
+```````````````````````````````` example alert
+> !NOTE
+> Should be blockquote
+.
+
+
!NOTE
+Should be blockquote
+
+````````````````````````````````
+
+Missing exclamation mark:
+
+```````````````````````````````` example alert
+> [NOTE]
+> Should be blockquote
+.
+
+
[NOTE]
+Should be blockquote
+
+````````````````````````````````
+
+Regular blockquote is not affected:
+
+```````````````````````````````` example alert
+> This is a regular blockquote
+.
+
+
This is a regular blockquote
+
+````````````````````````````````
+
+## Boundaries
+
+Trailing spaces after marker:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is a note
+.
+
+
Note
+
This is a note
+
+````````````````````````````````
+
+Trailing tabs after marker:
+
+```````````````````````````````` example alert
+> [!WARNING]→→
+> Be careful
+.
+
+
Warning
+
Be careful
+
+````````````````````````````````
+
+Leading spaces before blockquote marker:
+
+```````````````````````````````` example alert
+ > [!IMPORTANT]
+ > Content
+.
+
+
Important
+
Content
+
+````````````````````````````````
+
+Blank line after marker ends the blockquote (not an alert):
+
+```````````````````````````````` example alert
+> [!NOTE]
+
+Some text
+.
+
+
[!NOTE]
+
+
Some text
+````````````````````````````````
+
+Alert followed by blockquote:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is an alert
+
+> This is a blockquote
+.
+
+
Note
+
This is an alert
+
+
+
This is a blockquote
+
+````````````````````````````````
+
+Adjacent alerts:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> First alert
+
+> [!WARNING]
+> Second alert
+.
+
+
Note
+
First alert
+
+
+
Warning
+
Second alert
+
+````````````````````````````````
+
+## Nesting and containers
+
+Nested alert inside alert renders as blockquote:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is a note
+>> [!WARNING]
+>> Nested content
+.
+
+
Note
+
This is a note
+
+
[!WARNING]
+Nested content
+
+
+````````````````````````````````
+
+Nested blockquote inside alert:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is a note
+>> Nested blockquote
+.
+
+
Note
+
This is a note
+
+
Nested blockquote
+
+
+````````````````````````````````
+
+Alert inside list item stays as blockquote:
+
+```````````````````````````````` example alert
+- > [!NOTE]
+ > Test
+.
+
+
+
+
[!NOTE]
+Test
+
+
+
+````````````````````````````````
+
+Alert marker in content is treated as text:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> This is a note
+> [!WARNING]
+> This is still part of the note
+.
+
+
Note
+
This is a note
+[!WARNING]
+This is still part of the note
+
+````````````````````````````````
+
+## Continuation and interruption
+
+Lazy continuation:
+
+```````````````````````````````` example alert
+> [!NOTE]
+> First line
+Lazy continuation
+> Continues alert
+.
+
+
Note
+
First line
+Lazy continuation
+Continues alert
+
+````````````````````````````````
+
+Alert type after regular blockquote content is not an alert:
+
+```````````````````````````````` example alert
+> Regular blockquote
+> [!NOTE]
+> More text
+.
+
+
Regular blockquote
+[!NOTE]
+More text
+
+````````````````````````````````
diff --git a/commonmark-ext-gfm-alerts/src/test/resources/generate-alerts-spec.java b/commonmark-ext-gfm-alerts/src/test/resources/generate-alerts-spec.java
new file mode 100644
index 000000000..06192f107
--- /dev/null
+++ b/commonmark-ext-gfm-alerts/src/test/resources/generate-alerts-spec.java
@@ -0,0 +1,111 @@
+///usr/bin/env jbang "$0" "$@" ; exit $?
+
+// Generates alerts-spec.txt from alerts-spec-template.md by rendering each example
+// through the GitHub Markdown API and inserting the normalized HTML expectation.
+//
+// Prerequisites: gh CLI installed and authenticated (gh auth login)
+// Usage: cd commonmark-ext-gfm-alerts/src/test/resources && jbang generate-alerts-spec.java
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+class GenerateAlertsSpec {
+
+ private static final String FENCE = "````````````````````````````````";
+ private static final String EXAMPLE_OPEN = FENCE + " example alert";
+
+ public static void main(String[] args) throws Exception {
+ var templatePath = Path.of("alerts-spec-template.md");
+ if (!Files.exists(templatePath)) {
+ System.err.println("Run from the directory containing alerts-spec-template.md");
+ System.exit(1);
+ }
+
+ var lines = Files.readAllLines(templatePath);
+ var output = new ArrayList();
+ var header = "Expectations verified against GitHub Markdown API (gh api markdown -f mode=gfm).\n" +
+ "Our HTML omits GitHub's SVG icons and uses a `data-alert-type` attribute instead.";
+
+ int exampleCount = 0;
+ int i = 0;
+ while (i < lines.size()) {
+ var line = lines.get(i);
+
+ // Insert header after the first heading
+ if (i == 0 && line.startsWith("# ")) {
+ output.add(line);
+ output.add("");
+ output.add(header);
+ i++;
+ continue;
+ }
+
+ if (line.equals(EXAMPLE_OPEN)) {
+ // Collect source lines until closing fence
+ output.add(line);
+ i++;
+ var sourceLines = new ArrayList();
+ while (i < lines.size() && !lines.get(i).equals(FENCE)) {
+ sourceLines.add(lines.get(i));
+ output.add(lines.get(i));
+ i++;
+ }
+
+ // Render via GitHub API (→ represents tabs in the spec format)
+ var source = String.join("\n", sourceLines).replace("\u2192", "\t");
+ exampleCount++;
+ System.out.printf("%d: %s%n", exampleCount,
+ source.substring(0, Math.min(40, source.length())).replace("\n", "\\n"));
+
+ var ghHtml = normalizeHtml(renderViaGh(source));
+
+ // Insert separator and HTML expectation
+ output.add(".");
+ output.add(ghHtml);
+ output.add(FENCE);
+ i++; // skip closing fence from template
+ } else {
+ output.add(line);
+ i++;
+ }
+ }
+
+ var specPath = Path.of("alerts-spec.txt");
+ Files.writeString(specPath, String.join("\n", output) + "\n");
+ System.out.println("Done — " + exampleCount + " examples written to alerts-spec.txt");
+ }
+
+ static String renderViaGh(String markdown) throws Exception {
+ var process = new ProcessBuilder("gh", "api", "markdown", "-f", "mode=gfm", "-f", "text=" + markdown)
+ .redirectErrorStream(true)
+ .start();
+ var output = new String(process.getInputStream().readAllBytes());
+ if (process.waitFor() != 0) {
+ throw new RuntimeException("gh api failed: " + output);
+ }
+ return output;
+ }
+
+ // Normalize GitHub API HTML to match our renderer output.
+ static String normalizeHtml(String html) {
+ // Strip GitHub-specific elements and attributes
+ html = Pattern.compile("", Pattern.DOTALL).matcher(html).replaceAll("");
+ html = html.replaceAll(" (dir=\"auto\"|rel=\"nofollow\"|class=\"notranslate\")", "");
+ // Add data-alert-type and insert newlines to match our renderer's formatting
+ html = Pattern.compile("class=\"markdown-alert markdown-alert-(\\w+)\"")
+ .matcher(html)
+ .replaceAll("class=\"markdown-alert markdown-alert-$1\" data-alert-type=\"$1\"");
+ html = Pattern.compile("(data-alert-type=\"\\w+\">)(
", "
\n
");
+ return html.replace("\r\n", "\n").lines()
+ .map(String::stripTrailing)
+ .reduce((a, b) -> a + "\n" + b)
+ .orElse("")
+ .strip();
+ }
+}
\ No newline at end of file
diff --git a/commonmark-ext-gfm-strikethrough/.settings/org.eclipse.core.runtime.prefs b/commonmark-ext-gfm-strikethrough/.settings/org.eclipse.core.runtime.prefs
deleted file mode 100644
index 5a0ad22d2..000000000
--- a/commonmark-ext-gfm-strikethrough/.settings/org.eclipse.core.runtime.prefs
+++ /dev/null
@@ -1,2 +0,0 @@
-eclipse.preferences.version=1
-line.separator=\n
diff --git a/commonmark-ext-gfm-strikethrough/.settings/org.eclipse.jdt.core.prefs b/commonmark-ext-gfm-strikethrough/.settings/org.eclipse.jdt.core.prefs
deleted file mode 100644
index 3c0d27c8f..000000000
--- a/commonmark-ext-gfm-strikethrough/.settings/org.eclipse.jdt.core.prefs
+++ /dev/null
@@ -1,290 +0,0 @@
-eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.compliance=1.7
-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.7
-org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
-org.eclipse.jdt.core.formatter.alignment_for_assignment=0
-org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
-org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
-org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
-org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
-org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
-org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
-org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
-org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
-org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
-org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
-org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
-org.eclipse.jdt.core.formatter.blank_lines_after_package=1
-org.eclipse.jdt.core.formatter.blank_lines_before_field=0
-org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
-org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
-org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
-org.eclipse.jdt.core.formatter.blank_lines_before_method=1
-org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
-org.eclipse.jdt.core.formatter.blank_lines_before_package=0
-org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
-org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
-org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
-org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
-org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
-org.eclipse.jdt.core.formatter.comment.format_block_comments=true
-org.eclipse.jdt.core.formatter.comment.format_header=false
-org.eclipse.jdt.core.formatter.comment.format_html=true
-org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
-org.eclipse.jdt.core.formatter.comment.format_line_comments=true
-org.eclipse.jdt.core.formatter.comment.format_source_code=true
-org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
-org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
-org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
-org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
-org.eclipse.jdt.core.formatter.comment.line_length=120
-org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
-org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
-org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
-org.eclipse.jdt.core.formatter.compact_else_if=true
-org.eclipse.jdt.core.formatter.continuation_indentation=2
-org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
-org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
-org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
-org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
-org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
-org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
-org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
-org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
-org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
-org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
-org.eclipse.jdt.core.formatter.indent_empty_lines=false
-org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
-org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
-org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
-org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
-org.eclipse.jdt.core.formatter.indentation.size=4
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
-org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
-org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
-org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
-org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
-org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
-org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
-org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
-org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
-org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
-org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
-org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
-org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
-org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
-org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
-org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
-org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
-org.eclipse.jdt.core.formatter.join_lines_in_comments=true
-org.eclipse.jdt.core.formatter.join_wrapped_lines=false
-org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
-org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
-org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
-org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
-org.eclipse.jdt.core.formatter.lineSplit=120
-org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
-org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
-org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
-org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
-org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
-org.eclipse.jdt.core.formatter.tabulation.char=space
-org.eclipse.jdt.core.formatter.tabulation.size=4
-org.eclipse.jdt.core.formatter.use_on_off_tags=false
-org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
-org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
-org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
-org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
-org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
diff --git a/commonmark-ext-gfm-strikethrough/pom.xml b/commonmark-ext-gfm-strikethrough/pom.xml
index bd97e0704..9d8f55e5f 100644
--- a/commonmark-ext-gfm-strikethrough/pom.xml
+++ b/commonmark-ext-gfm-strikethrough/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.19.1-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-gfm-strikethrough
@@ -24,20 +24,4 @@
-
-
-
- org.apache.maven.plugins
- maven-jar-plugin
-
-
-
- org.commonmark.ext.gfm.strikethrough
-
-
-
-
-
-
-
diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/module-info.java b/commonmark-ext-gfm-strikethrough/src/main/java/module-info.java
new file mode 100644
index 000000000..b6204934b
--- /dev/null
+++ b/commonmark-ext-gfm-strikethrough/src/main/java/module-info.java
@@ -0,0 +1,5 @@
+module org.commonmark.ext.gfm.strikethrough {
+ exports org.commonmark.ext.gfm.strikethrough;
+
+ requires transitive org.commonmark;
+}
diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/Strikethrough.java b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/Strikethrough.java
index 115ae9ea4..0c24642bc 100644
--- a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/Strikethrough.java
+++ b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/Strikethrough.java
@@ -4,19 +4,23 @@
import org.commonmark.node.Delimited;
/**
- * A strikethrough node containing text and other inline nodes nodes as children.
+ * A strikethrough node containing text and other inline nodes as children.
*/
public class Strikethrough extends CustomNode implements Delimited {
- private static final String DELIMITER = "~~";
+ private String delimiter;
+
+ public Strikethrough(String delimiter) {
+ this.delimiter = delimiter;
+ }
@Override
public String getOpeningDelimiter() {
- return DELIMITER;
+ return delimiter;
}
@Override
public String getClosingDelimiter() {
- return DELIMITER;
+ return delimiter;
}
}
diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/StrikethroughExtension.java b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/StrikethroughExtension.java
index 3d0839f11..364205aed 100644
--- a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/StrikethroughExtension.java
+++ b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/StrikethroughExtension.java
@@ -1,42 +1,78 @@
package org.commonmark.ext.gfm.strikethrough;
import org.commonmark.Extension;
-import org.commonmark.renderer.text.TextContentRenderer;
-import org.commonmark.renderer.text.TextContentNodeRendererContext;
-import org.commonmark.renderer.text.TextContentNodeRendererFactory;
import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughDelimiterProcessor;
import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughHtmlNodeRenderer;
+import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughMarkdownNodeRenderer;
import org.commonmark.ext.gfm.strikethrough.internal.StrikethroughTextContentNodeRenderer;
-import org.commonmark.renderer.html.HtmlRenderer;
-import org.commonmark.renderer.html.HtmlNodeRendererContext;
-import org.commonmark.renderer.html.HtmlNodeRendererFactory;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
+import org.commonmark.renderer.html.HtmlNodeRendererContext;
+import org.commonmark.renderer.html.HtmlNodeRendererFactory;
+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 org.commonmark.renderer.text.TextContentNodeRendererContext;
+import org.commonmark.renderer.text.TextContentNodeRendererFactory;
+import org.commonmark.renderer.text.TextContentRenderer;
+
+import java.util.Set;
/**
- * Extension for GFM strikethrough using ~~ (GitHub Flavored Markdown).
+ * Extension for GFM strikethrough using {@code ~} or {@code ~~} (GitHub Flavored Markdown).
+ *
Example input:
+ *
{@code ~foo~ or ~~bar~~}
+ *
Example output (HTML):
+ *
{@code foo or bar}
*
- * Create it with {@link #create()} and then configure it on the builders
+ * Create the extension with {@link #create()} and then add it to the parser and renderer builders
* ({@link org.commonmark.parser.Parser.Builder#extensions(Iterable)},
* {@link HtmlRenderer.Builder#extensions(Iterable)}).
*
*
* The parsed strikethrough text regions are turned into {@link Strikethrough} nodes.
*
+ *
+ * If you have another extension that only uses a single tilde ({@code ~}) syntax, you will have to configure this
+ * {@link StrikethroughExtension} to only accept the double tilde syntax, like this:
+ *
+ * If you don't do that, there's a conflict between the two extensions and you will get an
+ * {@link IllegalArgumentException} when constructing the parser.
+ *
*/
public class StrikethroughExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
- TextContentRenderer.TextContentRendererExtension {
+ TextContentRenderer.TextContentRendererExtension, MarkdownRenderer.MarkdownRendererExtension {
- private StrikethroughExtension() {
+ private final boolean requireTwoTildes;
+
+ private StrikethroughExtension(Builder builder) {
+ this.requireTwoTildes = builder.requireTwoTildes;
}
+ /**
+ * @return the extension with default options
+ */
public static Extension create() {
- return new StrikethroughExtension();
+ return builder().build();
+ }
+
+ /**
+ * @return a builder to configure the behavior of the extension
+ */
+ public static Builder builder() {
+ return new Builder();
}
@Override
public void extend(Parser.Builder parserBuilder) {
- parserBuilder.customDelimiterProcessor(new StrikethroughDelimiterProcessor());
+ parserBuilder.customDelimiterProcessor(new StrikethroughDelimiterProcessor(requireTwoTildes));
}
@Override
@@ -58,4 +94,41 @@ public NodeRenderer create(TextContentNodeRendererContext context) {
}
});
}
+
+ @Override
+ public void extend(MarkdownRenderer.Builder rendererBuilder) {
+ rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
+ @Override
+ public NodeRenderer create(MarkdownNodeRendererContext context) {
+ return new StrikethroughMarkdownNodeRenderer(context);
+ }
+
+ @Override
+ public Set getSpecialCharacters() {
+ return Set.of('~');
+ }
+ });
+ }
+
+ public static class Builder {
+
+ private boolean requireTwoTildes = false;
+
+ /**
+ * @param requireTwoTildes Whether two tilde characters ({@code ~~}) are required for strikethrough or whether
+ * one is also enough. Default is {@code false}; both a single tilde and two tildes can be used for strikethrough.
+ * @return {@code this}
+ */
+ public Builder requireTwoTildes(boolean requireTwoTildes) {
+ this.requireTwoTildes = requireTwoTildes;
+ return this;
+ }
+
+ /**
+ * @return a configured extension
+ */
+ public Extension build() {
+ return new StrikethroughExtension(this);
+ }
+ }
}
diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughDelimiterProcessor.java b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughDelimiterProcessor.java
index 7d54eedf2..4657106ab 100644
--- a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughDelimiterProcessor.java
+++ b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughDelimiterProcessor.java
@@ -10,6 +10,16 @@
public class StrikethroughDelimiterProcessor implements DelimiterProcessor {
+ private final boolean requireTwoTildes;
+
+ public StrikethroughDelimiterProcessor() {
+ this(false);
+ }
+
+ public StrikethroughDelimiterProcessor(boolean requireTwoTildes) {
+ this.requireTwoTildes = requireTwoTildes;
+ }
+
@Override
public char getOpeningCharacter() {
return '~';
@@ -22,33 +32,34 @@ public char getClosingCharacter() {
@Override
public int getMinLength() {
- return 2;
+ return requireTwoTildes ? 2 : 1;
}
@Override
public int process(DelimiterRun openingRun, DelimiterRun closingRun) {
- if (openingRun.length() >= 2 && closingRun.length() >= 2) {
- // Use exactly two delimiters even if we have more, and don't care about internal openers/closers.
+ if (openingRun.length() == closingRun.length() && openingRun.length() <= 2) {
+ // GitHub only accepts either one or two delimiters, but not a mix or more than that.
Text opener = openingRun.getOpener();
// Wrap nodes between delimiters in strikethrough.
- Node strikethrough = new Strikethrough();
+ String delimiter = openingRun.length() == 1 ? opener.getLiteral() : opener.getLiteral() + opener.getLiteral();
+ Node strikethrough = new Strikethrough(delimiter);
SourceSpans sourceSpans = new SourceSpans();
- sourceSpans.addAllFrom(openingRun.getOpeners(2));
+ sourceSpans.addAllFrom(openingRun.getOpeners(openingRun.length()));
for (Node node : Nodes.between(opener, closingRun.getCloser())) {
strikethrough.appendChild(node);
sourceSpans.addAll(node.getSourceSpans());
}
- sourceSpans.addAllFrom(closingRun.getClosers(2));
+ sourceSpans.addAllFrom(closingRun.getClosers(closingRun.length()));
strikethrough.setSourceSpans(sourceSpans.getSourceSpans());
opener.insertAfter(strikethrough);
- return 2;
+ return openingRun.length();
} else {
return 0;
}
diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughHtmlNodeRenderer.java b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughHtmlNodeRenderer.java
index 4dd0de39b..b1a82cb03 100644
--- a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughHtmlNodeRenderer.java
+++ b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughHtmlNodeRenderer.java
@@ -1,10 +1,9 @@
package org.commonmark.ext.gfm.strikethrough.internal;
-import org.commonmark.renderer.html.HtmlWriter;
-import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.node.Node;
+import org.commonmark.renderer.html.HtmlNodeRendererContext;
+import org.commonmark.renderer.html.HtmlWriter;
-import java.util.Collections;
import java.util.Map;
public class StrikethroughHtmlNodeRenderer extends StrikethroughNodeRenderer {
@@ -19,7 +18,7 @@ public StrikethroughHtmlNodeRenderer(HtmlNodeRendererContext context) {
@Override
public void render(Node node) {
- Map attributes = context.extendAttributes(node, "del", Collections.emptyMap());
+ Map attributes = context.extendAttributes(node, "del", Map.of());
html.tag("del", attributes);
renderChildren(node);
html.tag("/del");
diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughMarkdownNodeRenderer.java b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughMarkdownNodeRenderer.java
new file mode 100644
index 000000000..1c91dd64f
--- /dev/null
+++ b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughMarkdownNodeRenderer.java
@@ -0,0 +1,34 @@
+package org.commonmark.ext.gfm.strikethrough.internal;
+
+import org.commonmark.ext.gfm.strikethrough.Strikethrough;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownWriter;
+
+public class StrikethroughMarkdownNodeRenderer extends StrikethroughNodeRenderer {
+
+ private final MarkdownNodeRendererContext context;
+ private final MarkdownWriter writer;
+
+ public StrikethroughMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
+ this.context = context;
+ this.writer = context.getWriter();
+ }
+
+ @Override
+ public void render(Node node) {
+ Strikethrough strikethrough = (Strikethrough) node;
+ writer.raw(strikethrough.getOpeningDelimiter());
+ renderChildren(node);
+ writer.raw(strikethrough.getClosingDelimiter());
+ }
+
+ private void renderChildren(Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ Node next = node.getNext();
+ context.render(node);
+ node = next;
+ }
+ }
+}
diff --git a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughNodeRenderer.java b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughNodeRenderer.java
index 4f3a12618..18ed21887 100644
--- a/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughNodeRenderer.java
+++ b/commonmark-ext-gfm-strikethrough/src/main/java/org/commonmark/ext/gfm/strikethrough/internal/StrikethroughNodeRenderer.java
@@ -4,13 +4,12 @@
import org.commonmark.node.Node;
import org.commonmark.renderer.NodeRenderer;
-import java.util.Collections;
import java.util.Set;
abstract class StrikethroughNodeRenderer implements NodeRenderer {
@Override
public Set> getNodeTypes() {
- return Collections.>singleton(Strikethrough.class);
+ return Set.of(Strikethrough.class);
}
}
diff --git a/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughMarkdownRendererTest.java b/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughMarkdownRendererTest.java
new file mode 100644
index 000000000..c497a4db3
--- /dev/null
+++ b/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughMarkdownRendererTest.java
@@ -0,0 +1,35 @@
+package org.commonmark.ext.gfm.strikethrough;
+
+import org.commonmark.Extension;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class StrikethroughMarkdownRendererTest {
+
+ private static final Set EXTENSIONS = Set.of(StrikethroughExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void testStrikethrough() {
+ assertRoundTrip("~foo~ ~bar~\n");
+ assertRoundTrip("~~foo~~ ~~bar~~\n");
+ assertRoundTrip("~~f\\~oo~~ ~~bar~~\n");
+
+ assertRoundTrip("\\~foo\\~\n");
+ }
+
+ protected String render(String source) {
+ return RENDERER.render(PARSER.parse(source));
+ }
+
+ private void assertRoundTrip(String input) {
+ String rendered = render(input);
+ assertThat(rendered).isEqualTo(input);
+ }
+}
diff --git a/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughSpecTest.java b/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughSpecTest.java
new file mode 100644
index 000000000..f1199b521
--- /dev/null
+++ b/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughSpecTest.java
@@ -0,0 +1,42 @@
+package org.commonmark.ext.gfm.strikethrough;
+
+import org.commonmark.Extension;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.commonmark.testutil.RenderingTestCase;
+import org.commonmark.testutil.TestResources;
+import org.commonmark.testutil.example.Example;
+import org.commonmark.testutil.example.ExampleReader;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.Parameter;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.List;
+import java.util.Set;
+
+@ParameterizedClass
+@MethodSource("data")
+public class StrikethroughSpecTest extends RenderingTestCase {
+
+ private static final Set EXTENSIONS = Set.of(StrikethroughExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Parameter
+ Example example;
+
+ static List data() {
+ return ExampleReader.readExamples(TestResources.getGfmSpec(), "strikethrough");
+ }
+
+ @Test
+ public void testHtmlRendering() {
+ assertRendering(example.getSource(), example.getHtml());
+ }
+
+ @Override
+ protected String render(String source) {
+ return RENDERER.render(PARSER.parse(source));
+ }
+}
diff --git a/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughTest.java b/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughTest.java
index e2e3b95c4..c29391cdd 100644
--- a/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughTest.java
+++ b/commonmark-ext-gfm-strikethrough/src/test/java/org/commonmark/ext/gfm/strikethrough/StrikethroughTest.java
@@ -4,34 +4,36 @@
import org.commonmark.node.Node;
import org.commonmark.node.Paragraph;
import org.commonmark.node.SourceSpan;
+import org.commonmark.node.Text;
import org.commonmark.parser.IncludeSourceSpans;
import org.commonmark.parser.Parser;
+import org.commonmark.parser.delimiter.DelimiterProcessor;
+import org.commonmark.parser.delimiter.DelimiterRun;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.text.TextContentRenderer;
import org.commonmark.testutil.RenderingTestCase;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
-import java.util.Arrays;
-import java.util.Collections;
+import java.util.List;
import java.util.Set;
-import static org.junit.Assert.assertEquals;
+import static org.assertj.core.api.Assertions.assertThat;
public class StrikethroughTest extends RenderingTestCase {
- private static final Set EXTENSIONS = Collections.singleton(StrikethroughExtension.create());
+ private static final Set EXTENSIONS = Set.of(StrikethroughExtension.create());
private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
private static final TextContentRenderer CONTENT_RENDERER = TextContentRenderer.builder()
.extensions(EXTENSIONS).build();
@Test
- public void oneTildeIsNotEnough() {
- assertRendering("~foo~", "
~foo~
\n");
+ public void oneTildeIsEnough() {
+ assertRendering("~foo~", "
foo
\n");
}
@Test
- public void twoTildesYay() {
+ public void twoTildesWorksToo() {
assertRendering("~~foo~~", "
foo
\n");
}
@@ -48,23 +50,22 @@ public void unmatched() {
@Test
public void threeInnerThree() {
- assertRendering("a ~~~foo~~~", "
a ~foo~
\n");
+ assertRendering("a ~~~foo~~~", "
a ~~~foo~~~
\n");
}
@Test
public void twoInnerThree() {
- assertRendering("~~foo~~~", "
foo~
\n");
+ assertRendering("~~foo~~~", "
~~foo~~~
\n");
}
@Test
public void tildesInside() {
assertRendering("~~foo~bar~~", "
foo~bar
\n");
assertRendering("~~foo~~bar~~", "
foobar~~
\n");
- assertRendering("~~foo~~~bar~~", "
foo~bar~~
\n");
- assertRendering("~~foo~~~~bar~~", "
foobar
\n");
- assertRendering("~~foo~~~~~bar~~", "
foo~bar
\n");
- assertRendering("~~foo~~~~~~bar~~", "
foo~~bar
\n");
- assertRendering("~~foo~~~~~~~bar~~", "
foo~~~bar
\n");
+ assertRendering("~~foo~~~bar~~", "
foo~~~bar
\n");
+ assertRendering("~~foo~~~~bar~~", "
foo~~~~bar
\n");
+ assertRendering("~~foo~~~~~bar~~", "
foo~~~~~bar
\n");
+ assertRendering("~~foo~~~~~~bar~~", "
foo~~~~~~bar
\n");
}
@Test
@@ -83,14 +84,27 @@ public void insideBlockQuote() {
public void delimited() {
Node document = PARSER.parse("~~foo~~");
Strikethrough strikethrough = (Strikethrough) document.getFirstChild().getFirstChild();
- assertEquals("~~", strikethrough.getOpeningDelimiter());
- assertEquals("~~", strikethrough.getClosingDelimiter());
+ assertThat(strikethrough.getOpeningDelimiter()).isEqualTo("~~");
+ assertThat(strikethrough.getClosingDelimiter()).isEqualTo("~~");
}
@Test
public void textContentRenderer() {
Node document = PARSER.parse("~~foo~~");
- assertEquals("/foo/", CONTENT_RENDERER.render(document));
+ assertThat(CONTENT_RENDERER.render(document)).isEqualTo("/foo/");
+ }
+
+ @Test
+ public void requireTwoTildesOption() {
+ Parser parser = Parser.builder()
+ .extensions(Set.of(StrikethroughExtension.builder()
+ .requireTwoTildes(true)
+ .build()))
+ .customDelimiterProcessor(new SubscriptDelimiterProcessor())
+ .build();
+
+ Node document = parser.parse("~foo~ ~~bar~~");
+ assertThat(CONTENT_RENDERER.render(document)).isEqualTo("(sub)foo(/sub) /bar/");
}
@Test
@@ -103,12 +117,36 @@ public void sourceSpans() {
Node document = parser.parse("hey ~~there~~\n");
Paragraph block = (Paragraph) document.getFirstChild();
Node strikethrough = block.getLastChild();
- assertEquals(Arrays.asList(SourceSpan.of(0, 4, 9)),
- strikethrough.getSourceSpans());
+ assertThat(strikethrough.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 4, 4, 9)));
}
@Override
protected String render(String source) {
return HTML_RENDERER.render(PARSER.parse(source));
}
+
+ private static 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) {
+ openingRun.getOpener().insertAfter(new Text("(sub)"));
+ closingRun.getCloser().insertBefore(new Text("(/sub)"));
+ return 1;
+ }
+ }
}
diff --git a/commonmark-ext-gfm-tables/.settings/org.eclipse.core.runtime.prefs b/commonmark-ext-gfm-tables/.settings/org.eclipse.core.runtime.prefs
deleted file mode 100644
index 5a0ad22d2..000000000
--- a/commonmark-ext-gfm-tables/.settings/org.eclipse.core.runtime.prefs
+++ /dev/null
@@ -1,2 +0,0 @@
-eclipse.preferences.version=1
-line.separator=\n
diff --git a/commonmark-ext-gfm-tables/.settings/org.eclipse.jdt.core.prefs b/commonmark-ext-gfm-tables/.settings/org.eclipse.jdt.core.prefs
deleted file mode 100644
index 3c0d27c8f..000000000
--- a/commonmark-ext-gfm-tables/.settings/org.eclipse.jdt.core.prefs
+++ /dev/null
@@ -1,290 +0,0 @@
-eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.compliance=1.7
-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.7
-org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
-org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
-org.eclipse.jdt.core.formatter.alignment_for_assignment=0
-org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
-org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
-org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=80
-org.eclipse.jdt.core.formatter.alignment_for_enum_constants=0
-org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
-org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0
-org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
-org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=80
-org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
-org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
-org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16
-org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
-org.eclipse.jdt.core.formatter.blank_lines_after_package=1
-org.eclipse.jdt.core.formatter.blank_lines_before_field=0
-org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
-org.eclipse.jdt.core.formatter.blank_lines_before_imports=1
-org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1
-org.eclipse.jdt.core.formatter.blank_lines_before_method=1
-org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
-org.eclipse.jdt.core.formatter.blank_lines_before_package=0
-org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
-org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1
-org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
-org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
-org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
-org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
-org.eclipse.jdt.core.formatter.comment.format_block_comments=true
-org.eclipse.jdt.core.formatter.comment.format_header=false
-org.eclipse.jdt.core.formatter.comment.format_html=true
-org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
-org.eclipse.jdt.core.formatter.comment.format_line_comments=true
-org.eclipse.jdt.core.formatter.comment.format_source_code=true
-org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true
-org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
-org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
-org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
-org.eclipse.jdt.core.formatter.comment.line_length=120
-org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true
-org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
-org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
-org.eclipse.jdt.core.formatter.compact_else_if=true
-org.eclipse.jdt.core.formatter.continuation_indentation=2
-org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
-org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off
-org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
-org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
-org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
-org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
-org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
-org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
-org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
-org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
-org.eclipse.jdt.core.formatter.indent_empty_lines=false
-org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
-org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
-org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
-org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
-org.eclipse.jdt.core.formatter.indentation.size=4
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_label=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
-org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
-org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
-org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
-org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
-org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
-org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
-org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
-org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
-org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
-org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
-org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
-org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
-org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert
-org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
-org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
-org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
-org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert
-org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
-org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
-org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
-org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
-org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert
-org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
-org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
-org.eclipse.jdt.core.formatter.join_lines_in_comments=true
-org.eclipse.jdt.core.formatter.join_wrapped_lines=false
-org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
-org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
-org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false
-org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
-org.eclipse.jdt.core.formatter.lineSplit=120
-org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
-org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
-org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
-org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1
-org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=true
-org.eclipse.jdt.core.formatter.tabulation.char=space
-org.eclipse.jdt.core.formatter.tabulation.size=4
-org.eclipse.jdt.core.formatter.use_on_off_tags=false
-org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
-org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
-org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
-org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
-org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter
diff --git a/commonmark-ext-gfm-tables/pom.xml b/commonmark-ext-gfm-tables/pom.xml
index 52e75ac90..5bd323168 100644
--- a/commonmark-ext-gfm-tables/pom.xml
+++ b/commonmark-ext-gfm-tables/pom.xml
@@ -4,7 +4,7 @@
org.commonmarkcommonmark-parent
- 0.19.1-SNAPSHOT
+ 0.28.1-SNAPSHOTcommonmark-ext-gfm-tables
@@ -24,20 +24,4 @@
-
-
-
- org.apache.maven.plugins
- maven-jar-plugin
-
-
-
- org.commonmark.ext.gfm.tables
-
-
-
-
-
-
-
diff --git a/commonmark-ext-gfm-tables/src/main/java/module-info.java b/commonmark-ext-gfm-tables/src/main/java/module-info.java
new file mode 100644
index 000000000..7e6d2629c
--- /dev/null
+++ b/commonmark-ext-gfm-tables/src/main/java/module-info.java
@@ -0,0 +1,5 @@
+module org.commonmark.ext.gfm.tables {
+ exports org.commonmark.ext.gfm.tables;
+
+ requires transitive org.commonmark;
+}
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TableCell.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TableCell.java
index 61880c6c3..033c2dd04 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TableCell.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TableCell.java
@@ -9,6 +9,7 @@ public class TableCell extends CustomNode {
private boolean header;
private Alignment alignment;
+ private int width;
/**
* @return whether the cell is a header or not
@@ -22,7 +23,7 @@ public void setHeader(boolean header) {
}
/**
- * @return the cell alignment
+ * @return the cell alignment or {@code null} if no specific alignment
*/
public Alignment getAlignment() {
return alignment;
@@ -32,6 +33,17 @@ public void setAlignment(Alignment alignment) {
this.alignment = alignment;
}
+ /**
+ * @return the cell width (the number of dash and colon characters in the delimiter row of the table for this column)
+ */
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
/**
* How the cell is aligned horizontally.
*/
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
index 5707b0f14..f754b8276 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/TablesExtension.java
@@ -3,16 +3,22 @@
import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.internal.TableBlockParser;
import org.commonmark.ext.gfm.tables.internal.TableHtmlNodeRenderer;
+import org.commonmark.ext.gfm.tables.internal.TableMarkdownNodeRenderer;
import org.commonmark.ext.gfm.tables.internal.TableTextContentNodeRenderer;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.NodeRenderer;
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlNodeRendererFactory;
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 org.commonmark.renderer.text.TextContentNodeRendererContext;
import org.commonmark.renderer.text.TextContentNodeRendererFactory;
import org.commonmark.renderer.text.TextContentRenderer;
+import java.util.Set;
+
/**
* Extension for GFM tables using "|" pipes (GitHub Flavored Markdown).
*
@@ -27,7 +33,7 @@
* @see Tables (extension) in GitHub Flavored Markdown Spec
*/
public class TablesExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
- TextContentRenderer.TextContentRendererExtension {
+ TextContentRenderer.TextContentRendererExtension, MarkdownRenderer.MarkdownRendererExtension {
private TablesExtension() {
}
@@ -60,4 +66,19 @@ public NodeRenderer create(TextContentNodeRendererContext context) {
}
});
}
+
+ @Override
+ public void extend(MarkdownRenderer.Builder rendererBuilder) {
+ rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() {
+ @Override
+ public NodeRenderer create(MarkdownNodeRendererContext context) {
+ return new TableMarkdownNodeRenderer(context);
+ }
+
+ @Override
+ public Set getSpecialCharacters() {
+ return Set.of('|');
+ }
+ });
+ }
}
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java
index a203164d3..57af128d8 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableBlockParser.java
@@ -1,7 +1,6 @@
package org.commonmark.ext.gfm.tables.internal;
import org.commonmark.ext.gfm.tables.*;
-import org.commonmark.internal.util.Parsing;
import org.commonmark.node.Block;
import org.commonmark.node.Node;
import org.commonmark.node.SourceSpan;
@@ -9,6 +8,7 @@
import org.commonmark.parser.SourceLine;
import org.commonmark.parser.SourceLines;
import org.commonmark.parser.block.*;
+import org.commonmark.text.Characters;
import java.util.ArrayList;
import java.util.List;
@@ -17,16 +17,18 @@ public class TableBlockParser extends AbstractBlockParser {
private final TableBlock block = new TableBlock();
private final List rowLines = new ArrayList<>();
- private final List columns;
+ private final List columns;
- private TableBlockParser(List columns, SourceLine headerLine) {
+ private boolean canHaveLazyContinuationLines = true;
+
+ private TableBlockParser(List columns, SourceLine headerLine) {
this.columns = columns;
this.rowLines.add(headerLine);
}
@Override
public boolean canHaveLazyContinuationLines() {
- return true;
+ return canHaveLazyContinuationLines;
}
@Override
@@ -36,7 +38,17 @@ public Block getBlock() {
@Override
public BlockContinue tryContinue(ParserState state) {
- if (Parsing.find('|', state.getLine().getContent(), 0) != -1) {
+ CharSequence content = state.getLine().getContent();
+ int pipe = Characters.find('|', content, state.getNextNonSpaceIndex());
+ if (pipe != -1) {
+ if (pipe == state.getNextNonSpaceIndex()) {
+ // If we *only* have a pipe character (and whitespace), that is not a valid table row and ends the table.
+ if (Characters.skipSpaceTab(content, pipe + 1, content.length()) == content.length()) {
+ // We also don't want the pipe to be added via lazy continuation.
+ canHaveLazyContinuationLines = false;
+ return BlockContinue.none();
+ }
+ }
return BlockContinue.atIndex(state.getIndex());
} else {
return BlockContinue.none();
@@ -108,12 +120,14 @@ private TableCell parseCell(SourceLine cell, int column, InlineParser inlinePars
}
if (column < columns.size()) {
- tableCell.setAlignment(columns.get(column));
+ TableCellInfo cellInfo = columns.get(column);
+ tableCell.setAlignment(cellInfo.getAlignment());
+ tableCell.setWidth(cellInfo.getWidth());
}
CharSequence content = cell.getContent();
- int start = Parsing.skipSpaceTab(content, 0, content.length());
- int end = Parsing.skipSpaceTabBackwards(content, content.length() - 1, start);
+ int start = Characters.skipSpaceTab(content, 0, content.length());
+ int end = Characters.skipSpaceTabBackwards(content, content.length() - 1, start);
inlineParser.parse(SourceLines.of(cell.substring(start, end + 1)), tableCell);
return tableCell;
@@ -121,14 +135,14 @@ private TableCell parseCell(SourceLine cell, int column, InlineParser inlinePars
private static List split(SourceLine line) {
CharSequence row = line.getContent();
- int nonSpace = Parsing.skipSpaceTab(row, 0, row.length());
+ int nonSpace = Characters.skipSpaceTab(row, 0, row.length());
int cellStart = nonSpace;
int cellEnd = row.length();
if (row.charAt(nonSpace) == '|') {
// This row has leading/trailing pipes - skip the leading pipe
cellStart = nonSpace + 1;
// Strip whitespace from the end but not the pipe or we could miss an empty ("||") cell
- int nonSpaceEnd = Parsing.skipSpaceTabBackwards(row, row.length() - 1, cellStart + 1);
+ int nonSpaceEnd = Characters.skipSpaceTabBackwards(row, row.length() - 1, cellStart);
cellEnd = nonSpaceEnd + 1;
}
List cells = new ArrayList<>();
@@ -175,11 +189,12 @@ private static List split(SourceLine line) {
// -|-
// |-|-|
// --- | ---
- private static List parseSeparator(CharSequence s) {
- List columns = new ArrayList<>();
+ private static List parseSeparator(CharSequence s) {
+ List columns = new ArrayList<>();
int pipes = 0;
boolean valid = false;
int i = 0;
+ int width = 0;
while (i < s.length()) {
char c = s.charAt(i);
switch (c) {
@@ -204,10 +219,12 @@ private static List parseSeparator(CharSequence s) {
if (c == ':') {
left = true;
i++;
+ width++;
}
boolean haveDash = false;
while (i < s.length() && s.charAt(i) == '-') {
i++;
+ width++;
haveDash = true;
}
if (!haveDash) {
@@ -217,8 +234,10 @@ private static List parseSeparator(CharSequence s) {
if (i < s.length() && s.charAt(i) == ':') {
right = true;
i++;
+ width++;
}
- columns.add(getAlignment(left, right));
+ columns.add(new TableCellInfo(getAlignment(left, right), width));
+ width = 0;
// Next, need another pipe
pipes = 0;
break;
@@ -255,21 +274,39 @@ public static class Factory extends AbstractBlockParserFactory {
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
List paragraphLines = matchedBlockParser.getParagraphLines().getLines();
- if (paragraphLines.size() == 1 && Parsing.find('|', paragraphLines.get(0).getContent(), 0) != -1) {
+ if (paragraphLines.size() >= 1 && Characters.find('|', paragraphLines.get(paragraphLines.size() - 1).getContent(), 0) != -1) {
SourceLine line = state.getLine();
SourceLine separatorLine = line.substring(state.getIndex(), line.getContent().length());
- List columns = parseSeparator(separatorLine.getContent());
+ List columns = parseSeparator(separatorLine.getContent());
if (columns != null && !columns.isEmpty()) {
- SourceLine paragraph = paragraphLines.get(0);
+ SourceLine paragraph = paragraphLines.get(paragraphLines.size() - 1);
List headerCells = split(paragraph);
if (columns.size() >= headerCells.size()) {
return BlockStart.of(new TableBlockParser(columns, paragraph))
.atIndex(state.getIndex())
- .replaceActiveBlockParser();
+ .replaceParagraphLines(1);
}
}
}
return BlockStart.none();
}
}
+
+ private static class TableCellInfo {
+ private final TableCell.Alignment alignment;
+ private final int width;
+
+ public TableCell.Alignment getAlignment() {
+ return alignment;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public TableCellInfo(TableCell.Alignment alignment, int width) {
+ this.alignment = alignment;
+ this.width = width;
+ }
+ }
}
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableHtmlNodeRenderer.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableHtmlNodeRenderer.java
index a1de50aac..966c4c151 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableHtmlNodeRenderer.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableHtmlNodeRenderer.java
@@ -5,7 +5,6 @@
import org.commonmark.renderer.html.HtmlNodeRendererContext;
import org.commonmark.renderer.html.HtmlWriter;
-import java.util.Collections;
import java.util.Map;
public class TableHtmlNodeRenderer extends TableNodeRenderer {
@@ -18,6 +17,7 @@ public TableHtmlNodeRenderer(HtmlNodeRendererContext context) {
this.context = context;
}
+ @Override
protected void renderBlock(TableBlock tableBlock) {
htmlWriter.line();
htmlWriter.tag("table", getAttributes(tableBlock, "table"));
@@ -26,6 +26,7 @@ protected void renderBlock(TableBlock tableBlock) {
htmlWriter.line();
}
+ @Override
protected void renderHead(TableHead tableHead) {
htmlWriter.line();
htmlWriter.tag("thead", getAttributes(tableHead, "thead"));
@@ -34,6 +35,7 @@ protected void renderHead(TableHead tableHead) {
htmlWriter.line();
}
+ @Override
protected void renderBody(TableBody tableBody) {
htmlWriter.line();
htmlWriter.tag("tbody", getAttributes(tableBody, "tbody"));
@@ -42,6 +44,7 @@ protected void renderBody(TableBody tableBody) {
htmlWriter.line();
}
+ @Override
protected void renderRow(TableRow tableRow) {
htmlWriter.line();
htmlWriter.tag("tr", getAttributes(tableRow, "tr"));
@@ -50,6 +53,7 @@ protected void renderRow(TableRow tableRow) {
htmlWriter.line();
}
+ @Override
protected void renderCell(TableCell tableCell) {
String tagName = tableCell.isHeader() ? "th" : "td";
htmlWriter.line();
@@ -60,14 +64,14 @@ protected void renderCell(TableCell tableCell) {
}
private Map getAttributes(Node node, String tagName) {
- return context.extendAttributes(node, tagName, Collections.emptyMap());
+ return context.extendAttributes(node, tagName, Map.of());
}
private Map getCellAttributes(TableCell tableCell, String tagName) {
if (tableCell.getAlignment() != null) {
- return context.extendAttributes(tableCell, tagName, Collections.singletonMap("align", getAlignValue(tableCell.getAlignment())));
+ return context.extendAttributes(tableCell, tagName, Map.of("align", getAlignValue(tableCell.getAlignment())));
} else {
- return context.extendAttributes(tableCell, tagName, Collections.emptyMap());
+ return context.extendAttributes(tableCell, tagName, Map.of());
}
}
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableMarkdownNodeRenderer.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableMarkdownNodeRenderer.java
new file mode 100644
index 000000000..b0705f579
--- /dev/null
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableMarkdownNodeRenderer.java
@@ -0,0 +1,88 @@
+package org.commonmark.ext.gfm.tables.internal;
+
+import org.commonmark.ext.gfm.tables.*;
+import org.commonmark.node.Node;
+import org.commonmark.renderer.markdown.MarkdownNodeRendererContext;
+import org.commonmark.renderer.markdown.MarkdownWriter;
+import org.commonmark.text.AsciiMatcher;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The Table node renderer that is needed for rendering GFM tables (GitHub Flavored Markdown) to text content.
+ */
+public class TableMarkdownNodeRenderer extends TableNodeRenderer {
+ private final MarkdownWriter writer;
+ private final MarkdownNodeRendererContext context;
+
+ private final AsciiMatcher pipe = AsciiMatcher.builder().c('|').build();
+
+ private final List columns = new ArrayList<>();
+
+ public TableMarkdownNodeRenderer(MarkdownNodeRendererContext context) {
+ this.writer = context.getWriter();
+ this.context = context;
+ }
+
+ @Override
+ protected void renderBlock(TableBlock node) {
+ columns.clear();
+ writer.pushTight(true);
+ renderChildren(node);
+ writer.popTight();
+ writer.block();
+ }
+
+ @Override
+ protected void renderHead(TableHead node) {
+ renderChildren(node);
+ for (TableCell.Alignment columnAlignment : columns) {
+ writer.raw('|');
+ if (columnAlignment == TableCell.Alignment.LEFT) {
+ writer.raw(":---");
+ } else if (columnAlignment == TableCell.Alignment.RIGHT) {
+ writer.raw("---:");
+ } else if (columnAlignment == TableCell.Alignment.CENTER) {
+ writer.raw(":---:");
+ } else {
+ writer.raw("---");
+ }
+ }
+ writer.raw("|");
+ writer.block();
+ }
+
+ @Override
+ protected void renderBody(TableBody node) {
+ renderChildren(node);
+ }
+
+ @Override
+ protected void renderRow(TableRow node) {
+ renderChildren(node);
+ // Trailing | at the end of the line
+ writer.raw("|");
+ writer.block();
+ }
+
+ @Override
+ protected void renderCell(TableCell node) {
+ if (node.getParent() != null && node.getParent().getParent() instanceof TableHead) {
+ columns.add(node.getAlignment());
+ }
+ writer.raw("|");
+ writer.pushRawEscape(pipe);
+ renderChildren(node);
+ writer.popRawEscape();
+ }
+
+ private void renderChildren(Node parent) {
+ Node node = parent.getFirstChild();
+ while (node != null) {
+ Node next = node.getNext();
+ context.render(node);
+ node = next;
+ }
+ }
+}
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableNodeRenderer.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableNodeRenderer.java
index 93478a30b..2982e1518 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableNodeRenderer.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableNodeRenderer.java
@@ -1,28 +1,22 @@
package org.commonmark.ext.gfm.tables.internal;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-import org.commonmark.ext.gfm.tables.TableBlock;
-import org.commonmark.ext.gfm.tables.TableBody;
-import org.commonmark.ext.gfm.tables.TableCell;
-import org.commonmark.ext.gfm.tables.TableHead;
-import org.commonmark.ext.gfm.tables.TableRow;
+import org.commonmark.ext.gfm.tables.*;
import org.commonmark.node.Node;
import org.commonmark.renderer.NodeRenderer;
+import java.util.Set;
+
abstract class TableNodeRenderer implements NodeRenderer {
@Override
public Set> getNodeTypes() {
- return new HashSet<>(Arrays.asList(
+ return Set.of(
TableBlock.class,
TableHead.class,
TableBody.class,
TableRow.class,
TableCell.class
- ));
+ );
}
@Override
diff --git a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableTextContentNodeRenderer.java b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableTextContentNodeRenderer.java
index 94b0e8665..0ba6894b5 100644
--- a/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableTextContentNodeRenderer.java
+++ b/commonmark-ext-gfm-tables/src/main/java/org/commonmark/ext/gfm/tables/internal/TableTextContentNodeRenderer.java
@@ -22,49 +22,46 @@ public TableTextContentNodeRenderer(TextContentNodeRendererContext context) {
this.context = context;
}
+ @Override
protected void renderBlock(TableBlock tableBlock) {
+ // Render rows tight
+ textContentWriter.pushTight(true);
renderChildren(tableBlock);
- if (tableBlock.getNext() != null) {
- textContentWriter.write("\n");
- }
+ textContentWriter.popTight();
+ textContentWriter.block();
}
+ @Override
protected void renderHead(TableHead tableHead) {
renderChildren(tableHead);
}
+ @Override
protected void renderBody(TableBody tableBody) {
renderChildren(tableBody);
}
+ @Override
protected void renderRow(TableRow tableRow) {
- textContentWriter.line();
renderChildren(tableRow);
- textContentWriter.line();
+ textContentWriter.block();
}
+ @Override
protected void renderCell(TableCell tableCell) {
renderChildren(tableCell);
- textContentWriter.write('|');
- textContentWriter.whitespace();
- }
-
- private void renderLastCell(TableCell tableCell) {
- renderChildren(tableCell);
+ // For the last cell in row, don't render the delimiter
+ if (tableCell.getNext() != null) {
+ textContentWriter.write('|');
+ textContentWriter.whitespace();
+ }
}
private void renderChildren(Node parent) {
Node node = parent.getFirstChild();
while (node != null) {
Node next = node.getNext();
-
- // For last cell in row, we dont render the delimiter.
- if (node instanceof TableCell && next == null) {
- renderLastCell((TableCell) node);
- } else {
- context.render(node);
- }
-
+ context.render(node);
node = next;
}
}
diff --git a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TableMarkdownRendererTest.java b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TableMarkdownRendererTest.java
new file mode 100644
index 000000000..85c11206c
--- /dev/null
+++ b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TableMarkdownRendererTest.java
@@ -0,0 +1,75 @@
+package org.commonmark.ext.gfm.tables;
+
+import org.commonmark.Extension;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.markdown.MarkdownRenderer;
+import org.junit.jupiter.api.Test;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TableMarkdownRendererTest {
+
+ private static final Set EXTENSIONS = Set.of(TablesExtension.create());
+ private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
+ private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build();
+
+ @Test
+ public void testHeadNoBody() {
+ assertRoundTrip("|Abc|\n|---|\n");
+ assertRoundTrip("|Abc|Def|\n|---|---|\n");
+ assertRoundTrip("|Abc||\n|---|---|\n");
+ }
+
+ @Test
+ public void testHeadAndBody() {
+ assertRoundTrip("|Abc|\n|---|\n|1|\n");
+ assertRoundTrip("|Abc|Def|\n|---|---|\n|1|2|\n");
+ }
+
+ @Test
+ public void testBodyHasFewerColumns() {
+ // Could try not to write empty trailing cells but this is fine too
+ assertRoundTrip("|Abc|Def|\n|---|---|\n|1||\n");
+ }
+
+ @Test
+ public void testAlignment() {
+ assertRoundTrip("|Abc|Def|\n|:---|---|\n|1|2|\n");
+ assertRoundTrip("|Abc|Def|\n|---|---:|\n|1|2|\n");
+ assertRoundTrip("|Abc|Def|\n|:---:|:---:|\n|1|2|\n");
+ }
+
+ @Test
+ public void testInsideBlockQuote() {
+ assertRoundTrip("> |Abc|Def|\n> |---|---|\n> |1|2|\n");
+ }
+
+ @Test
+ public void testMultipleTables() {
+ assertRoundTrip("|Abc|Def|\n|---|---|\n\n|One|\n|---|\n|Only|\n");
+ }
+
+ @Test
+ public void testEscaping() {
+ assertRoundTrip("|Abc|Def|\n|---|---|\n|Pipe in|text \\||\n");
+ assertRoundTrip("|Abc|Def|\n|---|---|\n|Pipe in|code `\\|`|\n");
+ assertRoundTrip("|Abc|Def|\n|---|---|\n|Inline HTML|Foo\\|bar|\n");
+ }
+
+ @Test
+ public void testEscaped() {
+ // `|` in Text nodes needs to be escaped, otherwise the generated Markdown does not get parsed back as a table
+ assertRoundTrip("\\|Abc\\|\n\\|---\\|\n");
+ }
+
+ protected String render(String source) {
+ return RENDERER.render(PARSER.parse(source));
+ }
+
+ private void assertRoundTrip(String input) {
+ String rendered = render(input);
+ assertThat(rendered).isEqualTo(input);
+ }
+}
diff --git a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesSpecTest.java b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesSpecTest.java
index 12c806e32..e7f3db4d1 100644
--- a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesSpecTest.java
+++ b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesSpecTest.java
@@ -7,39 +7,27 @@
import org.commonmark.testutil.TestResources;
import org.commonmark.testutil.example.Example;
import org.commonmark.testutil.example.ExampleReader;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.Parameter;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.provider.MethodSource;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import java.util.Set;
-@RunWith(Parameterized.class)
+@ParameterizedClass
+@MethodSource("data")
public class TablesSpecTest extends RenderingTestCase {
- private static final Set EXTENSIONS = Collections.singleton(TablesExtension.create());
+ private static final Set EXTENSIONS = Set.of(TablesExtension.create());
private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
- private final Example example;
+ @Parameter
+ Example example;
- public TablesSpecTest(Example example) {
- this.example = example;
- }
-
- @Parameters(name = "{0}")
- public static List