diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24160d342..b32794271 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11, 17, 21, 23] + java: [11, 17, 21, 25] steps: - name: Checkout sources uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4727b103d..c0531ca55 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,11 +19,12 @@ jobs: - name: Set up Maven Central repository uses: actions/setup-java@v4 with: - java-version: 11 + java-version: 24 distribution: 'zulu' - 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 + # 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 @@ -37,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/CHANGELOG.md b/CHANGELOG.md index 0808edb72..9c5c67268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,81 @@ with the exception that 0.x versions can break between minor versions. ## [Unreleased] ### Added -- More documentation with examples for `Node` classes +- Allow customizing HTML attributes for alert title `

` tag via `AttributeProvider` + +## [0.28.0] - 2026-03-31 +### Added +- New extension for alerts (aka callouts/admonitions) + - Syntax: + ``` + > [!NOTE] + > The text of the note. + ``` + - As types you can use NOTE, TIP, IMPORTANT, WARNING, CAUTION; or configure the + extension to add additional ones. + - Use class `AlertsExtension` in artifact `commonmark-ext-gfm-alerts` (#420) +- New option `maxOpenBlockParsers` for `Parser.Builder` to set an overall limit + for the depth of block parsing. If set, any nesting beyond the limit will be + parsed as paragraph text instead. The default remains unlimited. + +## [0.27.1] - 2026-01-14 +### Fixed +- Line(s) after a hard line break would sometimes also get an unwanted hard + line break, e.g. if they ended in emphasis or other non-text inlines (#415) +- `TextContentRenderer` (for plain text): Fix nested lists on the same line (#413) +- Fix minor performance regression with pathological input (deeply nested + brackets) that was introduced in version 0.23.0. + +## [0.27.0] - 2025-10-12 +### Added +- Autolink extension: Now supports configuration of different link types that + should be recognized and converted to links. See `AutolinkExtension#builder` + + | Type | Default? | Description | + |---------|----------|--------------------------------------------------------| + | `URL` | Yes | URL with a protocol such as `https://example.com` | + | `EMAIL` | Yes | Email address such as `foo@example.com` | + | `WWW` | Yes | Address beginning with `www` such as `www.example.com` | + + Note that this changes the behavior of `AutolinkExtension.create()` to now also + include `WWW` links by default. To re-enable the previous behavior, use: + + ```java + AutolinkExtension.builder().linkTypes(AutolinkType.URL, AutolinkType.EMAIL).build(); + ``` + +## [0.26.0] - 2025-09-13 +### Changed +- A `LinkProcessor` using `replaceWith` now also stops outer links from being + parsed as links, same as with `wrapTextIn`. This prevents nested links, see + footnotes change below. +### Fixed +- Fix rendering of image alt text to include contents of code spans (`` `code` ``). (#398) +- footnotes: Fix footnotes nested within links. Before, both the link and the + footnote reference would be parsed and lead to nested `` elements, which + is disallowed. Now, only the footnote is parsed and the outer link becomes + plain text; this matches the behavior of links. (#400) + +## [0.25.1] - 2025-08-01 +### Fixed +- footnotes: Fix parsing of footnote definitions containing multiple paragraphs + separated by blank lines. Before it only worked if paragraphs were separated + by lines of 4 spaces. (#388) + +## [0.25.0] - 2025-06-20 +### Added +- Include OSGi metadata in jars (`META-INF/MANIFEST.MF` files) (#378) +- More documentation with examples for `Node` classes (#370) ### Changed +- GitHub tables: Tables are now parsed even if there's no blank line before the + table heading, matching GitHub's behavior. (#381) ### Fixed - `MarkdownRenderer`: Fix precedence for `nodeRendererFactory`: Factories passed - to the builder can now override rendering for core node types. -- Fix warning in Eclipse about "missing 'requires transitive'" -- Fix Android incompatibility with `requireNonNullElseGet` + to the builder can now override rendering for core node types. (#368) +- `MarkdownRenderer`: Fix exception with ordered lists with a long first number + followed by a shorter one (#382) +- Fix warning in Eclipse about "missing 'requires transitive'" (#358) +- Fix Android incompatibility with `requireNonNullElseGet` (#369) ## [0.24.0] - 2024-10-21 ### Added @@ -469,7 +537,13 @@ API breaking changes (caused by changes in spec): Initial release of commonmark-java, a port of commonmark.js with extensions for autolinking URLs, GitHub flavored strikethrough and tables. -[Unreleased]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.24.0...main +[Unreleased]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.28.0...main +[0.28.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.27.1...commonmark-parent-0.28.0 +[0.27.1]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.27.0...commonmark-parent-0.27.1 +[0.27.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.26.0...commonmark-parent-0.27.0 +[0.26.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.25.1...commonmark-parent-0.26.0 +[0.25.1]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.25.0...commonmark-parent-0.25.1 +[0.25.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.24.0...commonmark-parent-0.25.0 [0.24.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.23.0...commonmark-parent-0.24.0 [0.23.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.22.0...commonmark-parent-0.23.0 [0.22.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.21.0...commonmark-parent-0.22.0 diff --git a/README.md b/README.md index 7660cc50c..845226729 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Coordinates for core library (see all on [Maven Central]): org.commonmark commonmark - 0.24.0 + 0.28.0 ``` @@ -291,7 +291,7 @@ First, add an additional dependency (see [Maven Central] for others): org.commonmark commonmark-ext-gfm-tables - 0.24.0 + 0.28.0 ``` @@ -333,6 +333,19 @@ Enables tables using pipes as in [GitHub Flavored Markdown][gfm-tables]. Use class `TablesExtension` in artifact `commonmark-ext-gfm-tables`. +### Alerts + +Adds support for GitHub-style alerts (also known as callouts or admonitions) as described [here](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts), e.g.: + +``` +> [!NOTE] +> The text of the note. +``` + +As types you can use NOTE, TIP, IMPORTANT, WARNING, CAUTION; or configure the extension to add additional ones. + +Use class `AlertsExtension` in artifact `commonmark-ext-gfm-alerts`. + ### Footnotes Enables footnotes like in [GitHub](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#footnotes) @@ -452,6 +465,9 @@ Some users of this library (feel free to raise a PR if you want to be added): * [Znai](https://github.com/testingisdocumenting/znai) * [Open Note](https://github.com/YangDai2003/OpenNote-Compose) a markdown editor and note-taking app for Android * [Quarkus Roq](https://github.com/quarkiverse/quarkus-roq/) The Roq Static Site Generator allows to easily create a static website or blog using Quarkus super-powers. +* [Lucee](https://github.com/lucee/lucee) +* [Previewer](https://github.com/sebthom/previewer-eclipse-plugin) an extensible Eclipse plugin that previews Markdown and other text based formats. +* [Xeres](https://xeres.io) a Peer-to-Peer application where all user generated content is done with markdown See also -------- diff --git a/commonmark-ext-autolink/pom.xml b/commonmark-ext-autolink/pom.xml index c2f9bf438..2cc4d53ca 100644 --- a/commonmark-ext-autolink/pom.xml +++ b/commonmark-ext-autolink/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-autolink @@ -12,7 +12,7 @@ commonmark-java extension for turning plain URLs and email addresses into links - 0.11.0 + 0.12.0 diff --git a/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/AutolinkExtension.java b/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/AutolinkExtension.java index e5926c7bb..7d5a74f30 100644 --- a/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/AutolinkExtension.java +++ b/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/AutolinkExtension.java @@ -1,5 +1,8 @@ package org.commonmark.ext.autolink; +import java.util.EnumSet; +import java.util.Set; + import org.commonmark.Extension; import org.commonmark.ext.autolink.internal.AutolinkPostProcessor; import org.commonmark.parser.Parser; @@ -18,16 +21,71 @@ */ public class AutolinkExtension implements Parser.ParserExtension { - private AutolinkExtension() { + private final Set linkTypes; + + private AutolinkExtension(Builder builder) { + this.linkTypes = builder.linkTypes; } + /** + * @return the extension with default options + */ public static Extension create() { - return new AutolinkExtension(); + 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.postProcessor(new AutolinkPostProcessor()); + parserBuilder.postProcessor(new AutolinkPostProcessor(linkTypes)); } + public static class Builder { + + private Set linkTypes = EnumSet.allOf(AutolinkType.class); + + /** + * @param linkTypes the link types that should be converted. By default, + * all {@link AutolinkType}s are converted. + * @return {@code this} + */ + public Builder linkTypes(AutolinkType... linkTypes) { + if (linkTypes == null) { + throw new NullPointerException("linkTypes must not be null"); + } + + return this.linkTypes(Set.of(linkTypes)); + } + + /** + * @param linkTypes the link types that should be converted. By default, + * all {@link AutolinkType}s are converted. + * @return {@code this} + */ + public Builder linkTypes(Set linkTypes) { + if (linkTypes == null) { + throw new NullPointerException("linkTypes must not be null"); + } + + if (linkTypes.isEmpty()) { + throw new IllegalArgumentException("linkTypes must not be empty"); + } + + this.linkTypes = EnumSet.copyOf(linkTypes); + return this; + } + + /** + * @return a configured extension + */ + public Extension build() { + return new AutolinkExtension(this); + } + } } diff --git a/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/AutolinkType.java b/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/AutolinkType.java new file mode 100644 index 000000000..2c8c6574f --- /dev/null +++ b/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/AutolinkType.java @@ -0,0 +1,19 @@ +package org.commonmark.ext.autolink; + +/** + * The types of strings that can be automatically turned into links. + */ +public enum AutolinkType { + /** + * URL such as {@code http://example.com} + */ + URL, + /** + * Email address such as {@code foo@example.com} + */ + EMAIL, + /** + * URL such as {@code www.example.com} + */ + WWW +} diff --git a/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/internal/AutolinkPostProcessor.java b/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/internal/AutolinkPostProcessor.java index ee8847911..a381c2f19 100644 --- a/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/internal/AutolinkPostProcessor.java +++ b/commonmark-ext-autolink/src/main/java/org/commonmark/ext/autolink/internal/AutolinkPostProcessor.java @@ -1,5 +1,6 @@ package org.commonmark.ext.autolink.internal; +import org.commonmark.ext.autolink.AutolinkType; import org.commonmark.node.*; import org.commonmark.parser.PostProcessor; import org.nibor.autolink.LinkExtractor; @@ -11,9 +12,36 @@ public class AutolinkPostProcessor implements PostProcessor { - private LinkExtractor linkExtractor = LinkExtractor.builder() - .linkTypes(EnumSet.of(LinkType.URL, LinkType.EMAIL)) - .build(); + private final LinkExtractor linkExtractor; + + public AutolinkPostProcessor(Set linkTypes) { + if (linkTypes == null) { + throw new NullPointerException("linkTypes must not be null"); + } + + if (linkTypes.isEmpty()) { + throw new IllegalArgumentException("linkTypes must not be empty"); + } + + var types = EnumSet.noneOf(LinkType.class); + for (AutolinkType linkType : linkTypes) { + switch (linkType) { + case URL: + types.add(LinkType.URL); + break; + case EMAIL: + types.add(LinkType.EMAIL); + break; + case WWW: + types.add(LinkType.WWW); + break; + } + } + + this.linkExtractor = LinkExtractor.builder() + .linkTypes(types) + .build(); + } @Override public Node process(Node node) { @@ -67,8 +95,13 @@ private static Text createTextNode(String literal, Span span, SourceSpan sourceS } private static String getDestination(LinkSpan linkSpan, String linkText) { - if (linkSpan.getType() == LinkType.EMAIL) { + var type = linkSpan.getType(); + + if (type == LinkType.EMAIL) { return "mailto:" + linkText; + } else if (type == LinkType.WWW) { + // Use http instead of https (see https://github.github.com/gfm/#extended-www-autolink) + return "http://" + linkText; } else { return linkText; } diff --git a/commonmark-ext-autolink/src/test/java/org/commonmark/ext/autolink/AutolinkTest.java b/commonmark-ext-autolink/src/test/java/org/commonmark/ext/autolink/AutolinkTest.java index 67fd69abe..82c3899fc 100644 --- a/commonmark-ext-autolink/src/test/java/org/commonmark/ext/autolink/AutolinkTest.java +++ b/commonmark-ext-autolink/src/test/java/org/commonmark/ext/autolink/AutolinkTest.java @@ -6,13 +6,12 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; import java.util.Set; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; public class AutolinkTest extends RenderingTestCase { @@ -20,6 +19,12 @@ public class AutolinkTest extends RenderingTestCase { private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build(); private static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build(); + private static final Set NO_WWW_EXTENSIONS = Set.of(AutolinkExtension.builder() + .linkTypes(AutolinkType.URL, AutolinkType.EMAIL) + .build()); + private static final Parser NO_WWW_PARSER = Parser.builder().extensions(NO_WWW_EXTENSIONS).build(); + private static final HtmlRenderer NO_WWW_RENDERER = HtmlRenderer.builder().extensions(NO_WWW_EXTENSIONS).build(); + @Test public void oneTextNode() { assertRendering("foo http://one.org/ bar http://two.org/", @@ -58,6 +63,18 @@ public void dontLinkTextWithinLinks() { "

http://example.com

\n"); } + @Test + public void wwwLinks() { + assertRendering("www.example.com", + "

www.example.com

\n"); + } + + @Test + public void noWwwLinks() { + String html = NO_WWW_RENDERER.render(NO_WWW_PARSER.parse("www.example.com")); + assertThat(html).isEqualTo("

www.example.com

\n"); + } + @Test public void sourceSpans() { Parser parser = Parser.builder() @@ -71,44 +88,37 @@ public void sourceSpans() { Paragraph paragraph = (Paragraph) document.getFirstChild(); Text abc = (Text) paragraph.getFirstChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 3)), - abc.getSourceSpans()); + assertThat(abc.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 3))); - assertTrue(abc.getNext() instanceof SoftLineBreak); + assertThat(abc.getNext()).isInstanceOf(SoftLineBreak.class); Link one = (Link) abc.getNext().getNext(); - assertEquals("http://example.com/one", one.getDestination()); - assertEquals(List.of(SourceSpan.of(1, 0, 4, 22)), - one.getSourceSpans()); + assertThat(one.getDestination()).isEqualTo("http://example.com/one"); + assertThat(one.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(1, 0, 4, 22))); - assertTrue(one.getNext() instanceof SoftLineBreak); + assertThat(one.getNext()).isInstanceOf(SoftLineBreak.class); Text def = (Text) one.getNext().getNext(); - assertEquals("def ", def.getLiteral()); - assertEquals(List.of(SourceSpan.of(2, 0, 27, 4)), - def.getSourceSpans()); + assertThat(def.getLiteral()).isEqualTo("def "); + assertThat(def.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 0, 27, 4))); Link two = (Link) def.getNext(); - assertEquals("http://example.com/two", two.getDestination()); - assertEquals(List.of(SourceSpan.of(2, 4, 31, 22)), - two.getSourceSpans()); + assertThat(two.getDestination()).isEqualTo("http://example.com/two"); + assertThat(two.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 4, 31, 22))); - assertTrue(two.getNext() instanceof SoftLineBreak); + assertThat(two.getNext()).isInstanceOf(SoftLineBreak.class); Text ghi = (Text) two.getNext().getNext(); - assertEquals("ghi ", ghi.getLiteral()); - assertEquals(List.of(SourceSpan.of(3, 0, 54, 4)), - ghi.getSourceSpans()); + assertThat(ghi.getLiteral()).isEqualTo("ghi "); + assertThat(ghi.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 0, 54, 4))); Link three = (Link) ghi.getNext(); - assertEquals("http://example.com/three", three.getDestination()); - assertEquals(List.of(SourceSpan.of(3, 4, 58, 24)), - three.getSourceSpans()); + assertThat(three.getDestination()).isEqualTo("http://example.com/three"); + assertThat(three.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 4, 58, 24))); Text jkl = (Text) three.getNext(); - assertEquals(" jkl", jkl.getLiteral()); - assertEquals(List.of(SourceSpan.of(3, 28, 82, 4)), - jkl.getSourceSpans()); + assertThat(jkl.getLiteral()).isEqualTo(" jkl"); + assertThat(jkl.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 28, 82, 4))); } @Override diff --git a/commonmark-ext-footnotes/pom.xml b/commonmark-ext-footnotes/pom.xml index d34e4e2f9..0d9e2f30c 100644 --- a/commonmark-ext-footnotes/pom.xml +++ b/commonmark-ext-footnotes/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-footnotes 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 index 59ee5529e..110bdef20 100644 --- 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 @@ -39,6 +39,10 @@ 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 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 index eddf6f2ea..3dcf4fc83 100644 --- 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 @@ -55,9 +55,7 @@ private void renderDefinition(FootnoteDefinition def) { writer.raw("]: "); writer.pushPrefix(" "); - writer.pushTight(true); renderChildren(def); - writer.popTight(); writer.popPrefix(); } 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 index ce5fdd51a..bc7d4f74c 100644 --- 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 @@ -8,7 +8,7 @@ import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.Asserts; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; import java.util.Set; diff --git a/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnoteMarkdownRendererTest.java b/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnoteMarkdownRendererTest.java index 9d567ece2..2f1125a02 100644 --- a/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnoteMarkdownRendererTest.java +++ b/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnoteMarkdownRendererTest.java @@ -4,11 +4,11 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; import org.commonmark.renderer.markdown.MarkdownRenderer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class FootnoteMarkdownRendererTest { private static final Set EXTENSIONS = Set.of(FootnotesExtension.builder().inlineFootnotes(true).build()); @@ -23,7 +23,7 @@ public void testSimple() { @Test public void testUnreferenced() { // Whether a reference has a corresponding definition or vice versa shouldn't matter for Markdown rendering. - assertRoundTrip("Test [^foo]\n\n[^foo]: one\n[^bar]: two\n"); + assertRoundTrip("Test [^foo]\n\n[^foo]: one\n\n[^bar]: two\n"); } @Test @@ -36,6 +36,18 @@ public void testBackslashInLabel() { assertRoundTrip("[^\\foo]\n\n[^\\foo]: note\n"); } + @Test + public void testMultipleLines() { + assertRoundTrip("Test [^1]\n\n[^1]: footnote l1\n footnote l2\n"); + } + + @Test + public void testMultipleParagraphs() { + // Note that the line between p1 and p2 could be blank too (instead of 4 spaces), but we currently don't + // preserve that information. + assertRoundTrip("Test [^1]\n\n[^1]: footnote p1\n \n footnote p2\n"); + } + @Test public void testInline() { assertRoundTrip("^[test *foo*]\n"); @@ -43,7 +55,7 @@ public void testInline() { private void assertRoundTrip(String input) { String rendered = parseAndRender(input); - assertEquals(input, rendered); + assertThat(rendered).isEqualTo(input); } private String parseAndRender(String source) { diff --git a/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnotesTest.java b/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnotesTest.java index bef6d89cb..7763cedb4 100644 --- a/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnotesTest.java +++ b/commonmark-ext-footnotes/src/test/java/org/commonmark/ext/footnotes/FootnotesTest.java @@ -4,15 +4,14 @@ import org.commonmark.node.*; import org.commonmark.parser.IncludeSourceSpans; import org.commonmark.parser.Parser; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.assertj.core.api.Assertions.assertThat; public class FootnotesTest { @@ -24,13 +23,13 @@ public void testDefBlockStart() { for (var s : List.of("1", "a", "^", "*", "\\a", "\uD83D\uDE42", "&0")) { var doc = PARSER.parse("[^" + s + "]: footnote\n"); var def = find(doc, FootnoteDefinition.class); - assertEquals(s, def.getLabel()); + assertThat(def.getLabel()).isEqualTo(s); } for (var s : List.of("", " ", "a b", "]", "\r", "\n", "\t")) { var input = "[^" + s + "]: footnote\n"; var doc = PARSER.parse(input); - assertNull("input: " + input, tryFind(doc, FootnoteDefinition.class)); + assertThat(tryFind(doc, FootnoteDefinition.class)).as("input: " + input).isNull(); } } @@ -40,24 +39,24 @@ public void testDefBlockStartInterrupts() { var doc = PARSER.parse("test\n[^1]: footnote\n"); var paragraph = find(doc, Paragraph.class); var def = find(doc, FootnoteDefinition.class); - assertEquals("test", ((Text) paragraph.getLastChild()).getLiteral()); - assertEquals("1", def.getLabel()); + assertThat(((Text) paragraph.getLastChild()).getLiteral()).isEqualTo("test"); + assertThat(def.getLabel()).isEqualTo("1"); } @Test public void testDefBlockStartIndented() { var doc1 = PARSER.parse(" [^1]: footnote\n"); - assertEquals("1", find(doc1, FootnoteDefinition.class).getLabel()); + assertThat(find(doc1, FootnoteDefinition.class).getLabel()).isEqualTo("1"); var doc2 = PARSER.parse(" [^1]: footnote\n"); - assertNull(tryFind(doc2, FootnoteDefinition.class)); + assertNone(doc2, FootnoteDefinition.class); } @Test public void testDefMultiple() { var doc = PARSER.parse("[^1]: foo\n[^2]: bar\n"); var defs = findAll(doc, FootnoteDefinition.class); - assertEquals("1", defs.get(0).getLabel()); - assertEquals("2", defs.get(1).getLabel()); + assertThat(defs.get(0).getLabel()).isEqualTo("1"); + assertThat(defs.get(1).getLabel()).isEqualTo("2"); } @Test @@ -65,8 +64,8 @@ public void testDefBlockStartAfterLinkReferenceDefinition() { var doc = PARSER.parse("[foo]: /url\n[^1]: footnote\n"); var linkReferenceDef = find(doc, LinkReferenceDefinition.class); var footnotesDef = find(doc, FootnoteDefinition.class); - assertEquals("foo", linkReferenceDef.getLabel()); - assertEquals("1", footnotesDef.getLabel()); + assertThat(linkReferenceDef.getLabel()).isEqualTo("foo"); + assertThat(footnotesDef.getLabel()).isEqualTo("1"); } @Test @@ -90,24 +89,44 @@ public void testDefContainsIndentedCodeBlock() { var doc = PARSER.parse("[^1]:\n code\n"); var def = find(doc, FootnoteDefinition.class); var codeBlock = (IndentedCodeBlock) def.getFirstChild(); - assertEquals("code\n", codeBlock.getLiteral()); + assertThat(codeBlock.getLiteral()).isEqualTo("code\n"); } @Test public void testDefContainsMultipleLines() { var doc = PARSER.parse("[^1]: footnote\nstill\n"); var def = find(doc, FootnoteDefinition.class); - assertEquals("1", def.getLabel()); + assertThat(def.getLabel()).isEqualTo("1"); var paragraph = (Paragraph) def.getFirstChild(); assertText("footnote", paragraph.getFirstChild()); assertText("still", paragraph.getLastChild()); } + @Test + public void testDefContainsMultipleParagraphs() { + var doc = PARSER.parse("[^1]: footnote p1\n\n footnote p2\n"); + var def = find(doc, FootnoteDefinition.class); + assertThat(def.getLabel()).isEqualTo("1"); + var p1 = (Paragraph) def.getFirstChild(); + assertText("footnote p1", p1.getFirstChild()); + var p2 = (Paragraph) p1.getNext(); + assertText("footnote p2", p2.getFirstChild()); + } + + @Test + public void testDefFollowedByParagraph() { + var doc = PARSER.parse("[^1]: footnote\n\nnormal paragraph\n"); + var def = find(doc, FootnoteDefinition.class); + assertThat(def.getLabel()).isEqualTo("1"); + assertText("footnote", def.getFirstChild().getFirstChild()); + assertText("normal paragraph", def.getNext().getFirstChild()); + } + @Test public void testDefContainsList() { var doc = PARSER.parse("[^1]: - foo\n - bar\n"); var def = find(doc, FootnoteDefinition.class); - assertEquals("1", def.getLabel()); + assertThat(def.getLabel()).isEqualTo("1"); var list = (BulletList) def.getFirstChild(); var item1 = (ListItem) list.getFirstChild(); var item2 = (ListItem) list.getLastChild(); @@ -120,7 +139,7 @@ public void testDefInterruptedByOthers() { var doc = PARSER.parse("[^1]: footnote\n# Heading\n"); var def = find(doc, FootnoteDefinition.class); var heading = find(doc, Heading.class); - assertEquals("1", def.getLabel()); + assertThat(def.getLabel()).isEqualTo("1"); assertText("Heading", heading.getFirstChild()); } @@ -128,13 +147,13 @@ public void testDefInterruptedByOthers() { public void testReference() { var doc = PARSER.parse("Test [^foo]\n\n[^foo]: /url\n"); var ref = find(doc, FootnoteReference.class); - assertEquals("foo", ref.getLabel()); + assertThat(ref.getLabel()).isEqualTo("foo"); } @Test public void testReferenceNoDefinition() { var doc = PARSER.parse("Test [^foo]\n"); - assertNull(tryFind(doc, FootnoteReference.class)); + assertNone(doc, FootnoteReference.class); } @Test @@ -142,13 +161,13 @@ public void testRefWithEmphasisInside() { // No emphasis inside footnote reference, should just be treated as text var doc = PARSER.parse("Test [^*foo*]\n\n[^*foo*]: def\n"); var ref = find(doc, FootnoteReference.class); - assertEquals("*foo*", ref.getLabel()); - assertNull(ref.getFirstChild()); + assertThat(ref.getLabel()).isEqualTo("*foo*"); + assertThat(ref.getFirstChild()).isNull(); var paragraph = doc.getFirstChild(); var text = (Text) paragraph.getFirstChild(); - assertEquals("Test ", text.getLiteral()); - assertEquals(ref, text.getNext()); - assertEquals(ref, paragraph.getLastChild()); + assertThat(text.getLiteral()).isEqualTo("Test "); + assertThat(text.getNext()).isEqualTo(ref); + assertThat(paragraph.getLastChild()).isEqualTo(ref); } @Test @@ -156,18 +175,18 @@ public void testRefWithEmphasisAround() { // Emphasis around footnote reference, the * inside needs to be removed from emphasis processing var doc = PARSER.parse("Test *abc [^foo*] def*\n\n[^foo*]: def\n"); var ref = find(doc, FootnoteReference.class); - assertEquals("foo*", ref.getLabel()); + assertThat(ref.getLabel()).isEqualTo("foo*"); assertText("abc ", ref.getPrevious()); assertText(" def", ref.getNext()); var em = find(doc, Emphasis.class); - assertEquals(em, ref.getParent()); + assertThat(ref.getParent()).isEqualTo(em); } @Test public void testRefAfterBang() { var doc = PARSER.parse("Test![^foo]\n\n[^foo]: def\n"); var ref = find(doc, FootnoteReference.class); - assertEquals("foo", ref.getLabel()); + assertThat(ref.getLabel()).isEqualTo("foo"); var paragraph = doc.getFirstChild(); assertText("Test!", paragraph.getFirstChild()); } @@ -178,7 +197,7 @@ public void testRefAsLabelOnly() { // resolve as footnotes. If `[foo][^bar]` fails to parse as a bracket, `[^bar]` by itself needs to be tried. var doc = PARSER.parse("Test [foo][^bar]\n\n[^bar]: footnote\n"); var ref = find(doc, FootnoteReference.class); - assertEquals("bar", ref.getLabel()); + assertThat(ref.getLabel()).isEqualTo("bar"); var paragraph = doc.getFirstChild(); assertText("Test [foo]", paragraph.getFirstChild()); } @@ -188,7 +207,7 @@ public void testRefWithEmptyLabel() { // [^bar] is a footnote but [] is just text, because collapsed reference links don't resolve as footnotes var doc = PARSER.parse("Test [^bar][]\n\n[^bar]: footnote\n"); var ref = find(doc, FootnoteReference.class); - assertEquals("bar", ref.getLabel()); + assertThat(ref.getLabel()).isEqualTo("bar"); var paragraph = doc.getFirstChild(); assertText("Test ", paragraph.getFirstChild()); assertText("[]", paragraph.getLastChild()); @@ -198,22 +217,22 @@ public void testRefWithEmptyLabel() { public void testRefWithBracket() { // Not a footnote, [ needs to be escaped var doc = PARSER.parse("Test [^f[oo]\n\n[^f[oo]: /url\n"); - assertNull(tryFind(doc, FootnoteReference.class)); + assertNone(doc, FootnoteReference.class); } @Test public void testRefWithBackslash() { var doc = PARSER.parse("[^\\foo]\n\n[^\\foo]: note\n"); var ref = find(doc, FootnoteReference.class); - assertEquals("\\foo", ref.getLabel()); + assertThat(ref.getLabel()).isEqualTo("\\foo"); var def = find(doc, FootnoteDefinition.class); - assertEquals("\\foo", def.getLabel()); + assertThat(def.getLabel()).isEqualTo("\\foo"); } @Test public void testPreferInlineLink() { var doc = PARSER.parse("Test [^bar](/url)\n\n[^bar]: footnote\n"); - assertNull(tryFind(doc, FootnoteReference.class)); + assertNone(doc, FootnoteReference.class); } @Test @@ -221,7 +240,7 @@ public void testPreferReferenceLink() { // This is tricky because `[^*foo*][foo]` is a valid link already. If `[foo]` was not defined, the first bracket // would be a footnote. var doc = PARSER.parse("Test [^*foo*][foo]\n\n[^*foo*]: /url\n\n[foo]: /url"); - assertNull(tryFind(doc, FootnoteReference.class)); + assertNone(doc, FootnoteReference.class); } @Test @@ -229,12 +248,35 @@ public void testReferenceLinkWithoutDefinition() { // Similar to previous test but there's no definition var doc = PARSER.parse("Test [^*foo*][foo]\n\n[^*foo*]: def\n"); var ref = find(doc, FootnoteReference.class); - assertEquals("*foo*", ref.getLabel()); + assertThat(ref.getLabel()).isEqualTo("*foo*"); var paragraph = (Paragraph) doc.getFirstChild(); assertText("Test ", paragraph.getFirstChild()); assertText("[foo]", paragraph.getLastChild()); } + @Test + public void testFootnoteInLink() { + // Expected to behave the same way as a link within a link, see https://spec.commonmark.org/0.31.2/#example-518 + // i.e. the first (inner) link is parsed, which means the outer one becomes plain text, as nesting links is not + // allowed. + var doc = PARSER.parse("[link with footnote ref [^1]](https://example.com)\n\n[^1]: footnote\n"); + var ref = find(doc, FootnoteReference.class); + assertThat(ref.getLabel()).isEqualTo("1"); + var paragraph = doc.getFirstChild(); + assertText("[link with footnote ref ", paragraph.getFirstChild()); + assertText("](https://example.com)", paragraph.getLastChild()); + } + + @Test + public void testFootnoteWithMarkerInLink() { + var doc = PARSER.parse("[link with footnote ref ![^1]](https://example.com)\n\n[^1]: footnote\n"); + var ref = find(doc, FootnoteReference.class); + assertThat(ref.getLabel()).isEqualTo("1"); + var paragraph = doc.getFirstChild(); + assertText("[link with footnote ref !", paragraph.getFirstChild()); + assertText("](https://example.com)", paragraph.getLastChild()); + } + @Test public void testInlineFootnote() { var extension = FootnotesExtension.builder().inlineFootnotes(true).build(); @@ -249,12 +291,12 @@ public void testInlineFootnote() { { var doc = parser.parse("Test \\^[not inline footnote]"); - assertNull(tryFind(doc, InlineFootnote.class)); + assertNone(doc, InlineFootnote.class); } { var doc = parser.parse("Test ^[not inline footnote"); - assertNull(tryFind(doc, InlineFootnote.class)); + assertNone(doc, InlineFootnote.class); var t = doc.getFirstChild().getFirstChild(); assertText("Test ^[not inline footnote", t); } @@ -269,7 +311,7 @@ public void testInlineFootnote() { var fn = find(doc, InlineFootnote.class); assertText("test ", fn.getFirstChild()); var code = fn.getFirstChild().getNext(); - assertEquals("bla]", ((Code) code).getLiteral()); + assertThat(((Code) code).getLiteral()).isEqualTo("bla]"); } { @@ -277,7 +319,7 @@ public void testInlineFootnote() { var fn = find(doc, InlineFootnote.class); assertText("with a ", fn.getFirstChild()); var link = fn.getFirstChild().getNext(); - assertEquals("url", ((Link) link).getDestination()); + assertThat(((Link) link).getDestination()).isEqualTo("url"); } } @@ -287,10 +329,14 @@ public void testSourcePositions() { var doc = parser.parse("Test [^foo]\n\n[^foo]: /url\n"); var ref = find(doc, FootnoteReference.class); - assertEquals(ref.getSourceSpans(), List.of(SourceSpan.of(0, 5, 5, 6))); + assertThat(ref.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 5, 5, 6))); var def = find(doc, FootnoteDefinition.class); - assertEquals(def.getSourceSpans(), List.of(SourceSpan.of(2, 0, 13, 12))); + assertThat(def.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 0, 13, 12))); + } + + private static void assertNone(Node parent, Class nodeClass) { + assertThat(tryFind(parent, nodeClass)).as(() -> "Node " + parent + " containing " + nodeClass).isNull(); } private static T find(Node parent, Class nodeClass) { @@ -315,6 +361,6 @@ private static List findAll(Node parent, Class nodeClass) { private static void assertText(String expected, Node node) { var text = (Text) node; - assertEquals(expected, text.getLiteral()); + assertThat(text.getLiteral()).isEqualTo(expected); } } diff --git a/commonmark-ext-gfm-alerts/README.md b/commonmark-ext-gfm-alerts/README.md new file mode 100644 index 000000000..2368812e5 --- /dev/null +++ b/commonmark-ext-gfm-alerts/README.md @@ -0,0 +1,74 @@ +# commonmark-ext-gfm-alerts + +Extension for [commonmark-java](https://github.com/commonmark/commonmark-java) that adds support for [GitHub Flavored Markdown alerts](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts). + +Enables highlighting important information using blockquote syntax with five standard alert types: NOTE, TIP, IMPORTANT, WARNING, and CAUTION. + +## Usage + +#### Markdown Syntax + +```markdown +> [!NOTE] +> Useful information + +> [!WARNING] +> Critical information +``` + +#### Standard GFM Types + +```java +var extension = AlertsExtension.create(); +var parser = Parser.builder().extensions(List.of(extension)).build(); +var renderer = HtmlRenderer.builder().extensions(List.of(extension)).build(); +``` + +#### Custom Alert Types + +Add custom types beyond the five standard GFM types: + +```java +var extension = AlertsExtension.builder() + .addCustomType("BUG", "Known Bug") + .build(); +``` + +Custom types must be UPPERCASE. Standard type titles can also be overridden for localization. + +#### Styling + +Alerts render as `
` elements with CSS classes: + +```html +
+

Note

+

Content

+
+``` + +Basic CSS example: + +```css +.markdown-alert { + padding: 0.5rem 1rem; + margin-bottom: 1rem; + border-left: 4px solid; +} + +.markdown-alert-note { border-color: #0969da; background-color: #ddf4ff; } +.markdown-alert-tip { border-color: #1a7f37; background-color: #dcffe4; } +.markdown-alert-important { border-color: #8250df; background-color: #f6f0ff; } +.markdown-alert-warning { border-color: #9a6700; background-color: #fff8c5; } +.markdown-alert-caution { border-color: #cf222e; background-color: #ffebe9; } +``` + +![Alerts](screenshots/alerts.png) + +Icons can be added using GitHub's [Octicons](https://primer.style/octicons/): + +![Alerts with icons](screenshots/alerts-with-icons.png) + +## License + +See the main commonmark-java project for license information. diff --git a/commonmark-ext-gfm-alerts/pom.xml b/commonmark-ext-gfm-alerts/pom.xml new file mode 100644 index 000000000..02ecbf802 --- /dev/null +++ b/commonmark-ext-gfm-alerts/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + org.commonmark + commonmark-parent + 0.28.1-SNAPSHOT + + + commonmark-ext-gfm-alerts + commonmark-java extension for alerts + commonmark-java extension for GFM alerts (admonition blocks) using [!TYPE] syntax (GitHub Flavored Markdown) + + + + org.commonmark + commonmark + + + + org.commonmark + commonmark-test-util + test + + + + diff --git a/commonmark-ext-gfm-alerts/screenshots/alerts-with-icons.png b/commonmark-ext-gfm-alerts/screenshots/alerts-with-icons.png new file mode 100644 index 000000000..47da9402b Binary files /dev/null and b/commonmark-ext-gfm-alerts/screenshots/alerts-with-icons.png differ diff --git a/commonmark-ext-gfm-alerts/screenshots/alerts.png b/commonmark-ext-gfm-alerts/screenshots/alerts.png new file mode 100644 index 000000000..83d4009f0 Binary files /dev/null and b/commonmark-ext-gfm-alerts/screenshots/alerts.png differ diff --git a/commonmark-ext-gfm-alerts/src/main/java/module-info.java b/commonmark-ext-gfm-alerts/src/main/java/module-info.java new file mode 100644 index 000000000..e8b5aecb7 --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module org.commonmark.ext.gfm.alerts { + exports org.commonmark.ext.gfm.alerts; + + requires transitive org.commonmark; +} diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/Alert.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/Alert.java new file mode 100644 index 000000000..bb28e7344 --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/Alert.java @@ -0,0 +1,19 @@ +package org.commonmark.ext.gfm.alerts; + +import org.commonmark.node.CustomBlock; + +/** + * Alert block for highlighting important information using {@code [!TYPE]} syntax. + */ +public class Alert extends CustomBlock { + + private final String type; + + public Alert(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertsExtension.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertsExtension.java new file mode 100644 index 000000000..3990034d2 --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/AlertsExtension.java @@ -0,0 +1,118 @@ +package org.commonmark.ext.gfm.alerts; + +import org.commonmark.Extension; +import org.commonmark.ext.gfm.alerts.internal.AlertPostProcessor; +import org.commonmark.ext.gfm.alerts.internal.AlertHtmlNodeRenderer; +import org.commonmark.ext.gfm.alerts.internal.AlertMarkdownNodeRenderer; +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 java.util.HashMap; +import java.util.Locale; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Extension for GFM alerts using {@code [!TYPE]} syntax (GitHub Flavored Markdown). + *

+ * Create with {@link #create()} or {@link #builder()} and configure on builders + * ({@link org.commonmark.parser.Parser.Builder#extensions(Iterable)}, + * {@link HtmlRenderer.Builder#extensions(Iterable)}). + * Parsed alerts become {@link Alert} blocks. + */ +public class AlertsExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension, + MarkdownRenderer.MarkdownRendererExtension { + + static final Set STANDARD_TYPES = Set.of("NOTE", "TIP", "IMPORTANT", "WARNING", "CAUTION"); + + private final Map customTypes; + + private AlertsExtension(Builder builder) { + this.customTypes = new HashMap<>(builder.customTypes); + } + + public static Extension create() { + return builder().build(); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public void extend(Parser.Builder parserBuilder) { + var allowedTypes = new HashSet<>(STANDARD_TYPES); + allowedTypes.addAll(customTypes.keySet()); + parserBuilder.postProcessor(new AlertPostProcessor(allowedTypes)); + } + + @Override + public void extend(HtmlRenderer.Builder rendererBuilder) { + rendererBuilder.nodeRendererFactory(new HtmlNodeRendererFactory() { + @Override + public NodeRenderer create(HtmlNodeRendererContext context) { + return new AlertHtmlNodeRenderer(context, customTypes); + } + }); + } + + @Override + public void extend(MarkdownRenderer.Builder rendererBuilder) { + rendererBuilder.nodeRendererFactory(new MarkdownNodeRendererFactory() { + @Override + public NodeRenderer create(MarkdownNodeRendererContext context) { + return new AlertMarkdownNodeRenderer(context); + } + + @Override + public Set getSpecialCharacters() { + return Set.of(); + } + }); + } + + /** + * Builder for configuring the alerts extension. + */ + public static class Builder { + private final Map customTypes = new HashMap<>(); + + /** + * Adds a custom alert type with a display title. + *

+ * This can also be used to override the display title of standard GFM types + * (e.g., for localization). + * + * @param type the alert type (must be uppercase) + * @param title the display title for this alert type + * @return {@code this} + */ + public Builder addCustomType(String type, String title) { + if (type == null || type.isEmpty()) { + throw new IllegalArgumentException("Type must not be null or empty"); + } + if (title == null || title.isEmpty()) { + throw new IllegalArgumentException("Title must not be null or empty"); + } + if (!type.equals(type.toUpperCase(Locale.ROOT))) { + throw new IllegalArgumentException("Type must be uppercase: " + type); + } + customTypes.put(type, title); + return this; + } + + /** + * @return a configured {@link Extension} + */ + public Extension build() { + return new AlertsExtension(this); + } + } +} diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertHtmlNodeRenderer.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertHtmlNodeRenderer.java new file mode 100644 index 000000000..ca562ba33 --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertHtmlNodeRenderer.java @@ -0,0 +1,78 @@ +package org.commonmark.ext.gfm.alerts.internal; + +import org.commonmark.ext.gfm.alerts.Alert; +import org.commonmark.node.Node; +import org.commonmark.renderer.html.HtmlNodeRendererContext; +import org.commonmark.renderer.html.HtmlWriter; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class AlertHtmlNodeRenderer extends AlertNodeRenderer { + + private final HtmlWriter htmlWriter; + private final HtmlNodeRendererContext context; + private final Map customTypeTitles; + + public AlertHtmlNodeRenderer(HtmlNodeRendererContext context, Map customTypeTitles) { + this.htmlWriter = context.getWriter(); + this.context = context; + this.customTypeTitles = customTypeTitles; + } + + @Override + protected void renderAlert(Alert alert) { + var type = alert.getType(); + var cssClass = type.toLowerCase(); + + htmlWriter.line(); + var attributes = new LinkedHashMap(); + attributes.put("class", "markdown-alert markdown-alert-" + cssClass); + attributes.put("data-alert-type", cssClass); + + htmlWriter.tag("div", context.extendAttributes(alert, "div", attributes)); + htmlWriter.line(); + + // Render alert title + htmlWriter.tag("p", context.extendAttributes(alert, "p", Map.of("class", "markdown-alert-title"))); + htmlWriter.text(getAlertTitle(type)); + htmlWriter.tag("/p"); + htmlWriter.line(); + + // Render children (the alert content) + renderChildren(alert); + + htmlWriter.tag("/div"); + htmlWriter.line(); + } + + private String getAlertTitle(String type) { + var customTypeTitle = customTypeTitles.get(type); + if (customTypeTitle != null) { + return customTypeTitle; + } + switch (type) { + case "NOTE": + return "Note"; + case "TIP": + return "Tip"; + case "IMPORTANT": + return "Important"; + case "WARNING": + return "Warning"; + case "CAUTION": + return "Caution"; + default: + throw new IllegalStateException("Unknown alert type: " + type); + } + } + + private void renderChildren(Node parent) { + var node = parent.getFirstChild(); + while (node != null) { + var next = node.getNext(); + context.render(node); + node = next; + } + } +} diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertMarkdownNodeRenderer.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertMarkdownNodeRenderer.java new file mode 100644 index 000000000..e3da62aea --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertMarkdownNodeRenderer.java @@ -0,0 +1,38 @@ +package org.commonmark.ext.gfm.alerts.internal; + +import org.commonmark.ext.gfm.alerts.Alert; +import org.commonmark.node.Node; +import org.commonmark.renderer.markdown.MarkdownNodeRendererContext; +import org.commonmark.renderer.markdown.MarkdownWriter; + +public class AlertMarkdownNodeRenderer extends AlertNodeRenderer { + + private final MarkdownWriter writer; + private final MarkdownNodeRendererContext context; + + public AlertMarkdownNodeRenderer(MarkdownNodeRendererContext context) { + this.writer = context.getWriter(); + this.context = context; + } + + @Override + protected void renderAlert(Alert alert) { + // First line: > [!TYPE] + writer.writePrefix("> "); + writer.pushPrefix("> "); + writer.raw("[!" + alert.getType() + "]"); + writer.line(); + renderChildren(alert); + writer.popPrefix(); + writer.block(); + } + + private void renderChildren(Node parent) { + var node = parent.getFirstChild(); + while (node != null) { + var next = node.getNext(); + context.render(node); + node = next; + } + } +} diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertNodeRenderer.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertNodeRenderer.java new file mode 100644 index 000000000..45b34bb46 --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertNodeRenderer.java @@ -0,0 +1,23 @@ +package org.commonmark.ext.gfm.alerts.internal; + +import org.commonmark.ext.gfm.alerts.Alert; +import org.commonmark.node.Node; +import org.commonmark.renderer.NodeRenderer; + +import java.util.Set; + +public abstract class AlertNodeRenderer implements NodeRenderer { + + @Override + public Set> getNodeTypes() { + return Set.of(Alert.class); + } + + @Override + public void render(Node node) { + var alert = (Alert) node; + renderAlert(alert); + } + + protected abstract void renderAlert(Alert alert); +} diff --git a/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertPostProcessor.java b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertPostProcessor.java new file mode 100644 index 000000000..8008fc8dd --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/main/java/org/commonmark/ext/gfm/alerts/internal/AlertPostProcessor.java @@ -0,0 +1,111 @@ +package org.commonmark.ext.gfm.alerts.internal; + +import org.commonmark.ext.gfm.alerts.Alert; +import org.commonmark.node.BlockQuote; +import org.commonmark.node.HardLineBreak; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; +import org.commonmark.node.SoftLineBreak; +import org.commonmark.node.Text; +import org.commonmark.parser.PostProcessor; + +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; + +public class AlertPostProcessor implements PostProcessor { + + // Alert type marker, matching any case (GitHub supports lowercase, mixed, and uppercase) + private static final Pattern ALERT_PATTERN = Pattern.compile("^\\[!([a-zA-Z]+)]\\s*$"); + + private final Set allowedTypes; + + public AlertPostProcessor(Set allowedTypes) { + this.allowedTypes = allowedTypes; + } + + @Override + public Node process(Node document) { + // Only look at direct children of Document — GitHub only detects alerts at the top level. + var child = document.getFirstChild(); + while (child != null) { + var next = child.getNext(); + if (child instanceof BlockQuote) { + tryConvertToAlert((BlockQuote) child); + } + child = next; + } + return document; + } + + private void tryConvertToAlert(BlockQuote blockQuote) { + var firstChild = blockQuote.getFirstChild(); + if (!(firstChild instanceof Paragraph)) { + return; + } + + var paragraph = (Paragraph) firstChild; + var firstInline = paragraph.getFirstChild(); + if (!(firstInline instanceof Text)) { + return; + } + + var textNode = (Text) firstInline; + + // The alert marker can be the entire text node content, or just the first line + // before a line break (trailing spaces create a HardLineBreak instead of SoftLineBreak). + var afterMarker = firstInline.getNext(); + if (afterMarker != null && !(afterMarker instanceof SoftLineBreak) && !(afterMarker instanceof HardLineBreak)) { + // Text followed by something other than a line break - not an alert + return; + } + + var matcher = ALERT_PATTERN.matcher(textNode.getLiteral()); + if (!matcher.matches()) { + return; + } + + var type = matcher.group(1).toUpperCase(Locale.ROOT); + if (!allowedTypes.contains(type)) { + return; + } + + // Must have content after the marker line. An alert with ONLY the marker + // and no content is a normal blockquote on GitHub. + if (afterMarker != null) { + // There's a line break after marker - check if there's content after it + if (afterMarker.getNext() == null && paragraph.getNext() == null) { + return; + } + afterMarker.unlink(); + } else { + // Marker is the only thing in the paragraph + if (paragraph.getNext() == null) { + return; + } + } + + // Valid alert. Create Alert node and transfer children. + var alert = new Alert(type); + alert.setSourceSpans(blockQuote.getSourceSpans()); + blockQuote.insertAfter(alert); + + // Remove the marker text from the first paragraph + firstInline.unlink(); + + // If paragraph is now empty, remove it + if (paragraph.getFirstChild() == null) { + paragraph.unlink(); + } + + // Move remaining children from blockquote to alert + var child = blockQuote.getFirstChild(); + while (child != null) { + var next = child.getNext(); + alert.appendChild(child); + child = next; + } + + blockQuote.unlink(); + } +} diff --git a/commonmark-ext-gfm-alerts/src/main/javadoc/overview.html b/commonmark-ext-gfm-alerts/src/main/javadoc/overview.html new file mode 100644 index 000000000..145232a87 --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/main/javadoc/overview.html @@ -0,0 +1,6 @@ + + +Extension for GitHub Flavored Markdown (GFM) alerts using blockquote syntax +

See {@link org.commonmark.ext.gfm.alerts.AlertsExtension}

+ + diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsMarkdownRendererTest.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsMarkdownRendererTest.java new file mode 100644 index 000000000..aca90e2df --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsMarkdownRendererTest.java @@ -0,0 +1,73 @@ +package org.commonmark.ext.gfm.alerts; + +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 AlertsMarkdownRendererTest { + + private static final Set EXTENSIONS = Set.of(AlertsExtension.create()); + private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build(); + private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build(); + + @Test + public void alertRoundTrip() { + assertRoundTrip("> [!WARNING]\n> Be careful\n"); + } + + @Test + public void allStandardTypesRoundTrip() { + assertRoundTrip("> [!NOTE]\n> Note\n"); + assertRoundTrip("> [!TIP]\n> Tip\n"); + assertRoundTrip("> [!IMPORTANT]\n> Important\n"); + assertRoundTrip("> [!WARNING]\n> Warning\n"); + assertRoundTrip("> [!CAUTION]\n> Caution\n"); + } + + @Test + public void lowercaseTypeRendersAsUppercase() { + // Lowercase input gets normalized to uppercase type + String rendered = RENDERER.render(PARSER.parse("> [!note]\n> Content\n")); + assertThat(rendered).isEqualTo("> [!NOTE]\n> Content\n"); + } + + @Test + public void alertWithMultipleParagraphs() { + String input = "> [!NOTE]\n> First paragraph\n>\n> Second paragraph\n"; + // MarkdownWriter always writes the prefix including trailing space + String expected = "> [!NOTE]\n> First paragraph\n> \n> Second paragraph\n"; + String rendered = RENDERER.render(PARSER.parse(input)); + assertThat(rendered).isEqualTo(expected); + } + + @Test + public void customTypeRoundTrip() { + Extension extension = AlertsExtension.builder() + .addCustomType("INFO", "Information") + .build(); + + Parser parser = Parser.builder().extensions(Set.of(extension)).build(); + MarkdownRenderer renderer = MarkdownRenderer.builder().extensions(Set.of(extension)).build(); + + String input = "> [!INFO]\n> Custom type\n"; + String rendered = renderer.render(parser.parse(input)); + assertThat(rendered).isEqualTo(input); + } + + @Test + public void alertWithList() { + String input = "> [!NOTE]\n> Items:\n> \n> - First\n> - Second\n"; + String rendered = RENDERER.render(PARSER.parse(input)); + assertThat(rendered).isEqualTo(input); + } + + private void assertRoundTrip(String input) { + String rendered = RENDERER.render(PARSER.parse(input)); + assertThat(rendered).isEqualTo(input); + } +} diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsSpecTest.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsSpecTest.java new file mode 100644 index 000000000..8155d8009 --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsSpecTest.java @@ -0,0 +1,44 @@ +package org.commonmark.ext.gfm.alerts; + +import org.commonmark.Extension; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.commonmark.testutil.RenderingTestCase; +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.net.URL; +import java.util.List; +import java.util.Set; + +@ParameterizedClass +@MethodSource("data") +public class AlertsSpecTest extends RenderingTestCase { + + private static final Set EXTENSIONS = Set.of(AlertsExtension.create()); + private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build(); + // Use softbreak("
") to match GitHub's rendering for easier comparison with GitHub API output. + private static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).softbreak("
\n").build(); + + @Parameter + Example example; + + static List data() { + URL spec = AlertsSpecTest.class.getResource("/alerts-spec.txt"); + return ExampleReader.readExamples(spec, "alert"); + } + + @Test + public void testHtmlRendering() { + assertRendering(example.getSource(), example.getHtml()); + } + + @Override + protected String render(String source) { + return RENDERER.render(PARSER.parse(source)); + } +} \ No newline at end of file diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsTest.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsTest.java new file mode 100644 index 000000000..c46c532fe --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/AlertsTest.java @@ -0,0 +1,140 @@ +package org.commonmark.ext.gfm.alerts; + +import org.commonmark.Extension; +import org.commonmark.node.Node; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AlertsTest { + + private static final Set EXTENSIONS = Set.of(AlertsExtension.create()); + private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build(); + + // Custom types + + @Test + public void customType() { + Extension extension = AlertsExtension.builder() + .addCustomType("INFO", "Information") + .build(); + + Parser parser = Parser.builder().extensions(Set.of(extension)).build(); + HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build(); + + assertThat(renderer.render(parser.parse("> [!INFO]\n> Custom alert"))).isEqualTo( + "
\n" + + "

Information

\n" + + "

Custom alert

\n" + + "
\n"); + } + + @Test + public void multipleCustomTypes() { + Extension extension = AlertsExtension.builder() + .addCustomType("INFO", "Information") + .addCustomType("SUCCESS", "Success!") + .addCustomType("DANGER", "Danger!") + .build(); + + Parser parser = Parser.builder().extensions(Set.of(extension)).build(); + HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build(); + + assertThat(renderer.render(parser.parse("> [!INFO]\n> Info content\n\n> [!SUCCESS]\n> Success content\n\n> [!DANGER]\n> Danger content"))).isEqualTo( + "
\n" + + "

Information

\n" + + "

Info content

\n" + + "
\n" + + "
\n" + + "

Success!

\n" + + "

Success content

\n" + + "
\n" + + "
\n" + + "

Danger!

\n" + + "

Danger content

\n" + + "
\n"); + } + + @Test + public void standardTypesWithCustomConfigured() { + Extension extension = AlertsExtension.builder() + .addCustomType("INFO", "Information") + .build(); + + Parser parser = Parser.builder().extensions(Set.of(extension)).build(); + HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build(); + + assertThat(renderer.render(parser.parse("> [!NOTE]\n> Standard type"))).isEqualTo( + "
\n" + + "

Note

\n" + + "

Standard type

\n" + + "
\n"); + } + + @Test + public void overrideStandardTypeTitle() { + Extension extension = AlertsExtension.builder() + .addCustomType("NOTE", "Nota") + .build(); + + Parser parser = Parser.builder().extensions(Set.of(extension)).build(); + HtmlRenderer renderer = HtmlRenderer.builder().extensions(Set.of(extension)).build(); + + assertThat(renderer.render(parser.parse("> [!NOTE]\n> Localized title"))).isEqualTo( + "
\n" + + "

Nota

\n" + + "

Localized title

\n" + + "
\n"); + } + + // Custom type validation + + @Test + public void customTypeMustBeUppercase() { + assertThrows(IllegalArgumentException.class, () -> + AlertsExtension.builder().addCustomType("info", "Information").build()); + } + + @Test + public void customTypeMustNotBeEmpty() { + assertThrows(IllegalArgumentException.class, () -> + AlertsExtension.builder().addCustomType("", "Title").build()); + } + + @Test + public void customTypeTitleMustNotBeEmpty() { + assertThrows(IllegalArgumentException.class, () -> + AlertsExtension.builder().addCustomType("INFO", "").build()); + } + + // AST + + @Test + public void alertParsedAsAlertNode() { + Node document = PARSER.parse("> [!NOTE]\n> This is a note"); + Node firstChild = document.getFirstChild(); + assertThat(firstChild).isInstanceOf(Alert.class); + Alert alert = (Alert) firstChild; + assertThat(alert.getType()).isEqualTo("NOTE"); + } + + @Test + public void customTypeParsedAsAlertNode() { + Extension extension = AlertsExtension.builder() + .addCustomType("INFO", "Information") + .build(); + + Parser parser = Parser.builder().extensions(Set.of(extension)).build(); + + Node document = parser.parse("> [!INFO]\n> Custom alert"); + Alert alert = (Alert) document.getFirstChild(); + + assertThat(alert.getType()).isEqualTo("INFO"); + } + +} \ No newline at end of file diff --git a/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/examples/AlertsExample.java b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/examples/AlertsExample.java new file mode 100644 index 000000000..34b78385c --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/test/java/org/commonmark/ext/gfm/alerts/examples/AlertsExample.java @@ -0,0 +1,85 @@ +package org.commonmark.ext.gfm.alerts.examples; + +import org.commonmark.ext.gfm.alerts.AlertsExtension; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; + +import java.util.List; + +/** + * Example demonstrating the use of the GFM Alerts extension. + */ +public class AlertsExample { + + public static void main(String[] args) { + standardTypesExample(); + System.out.println("\n" + "=".repeat(60) + "\n"); + customTypesExample(); + } + + private static void standardTypesExample() { + System.out.println("STANDARD GFM ALERT TYPES"); + System.out.println("=".repeat(60)); + + var extension = AlertsExtension.create(); + + var parser = Parser.builder() + .extensions(List.of(extension)) + .build(); + + var renderer = HtmlRenderer.builder() + .extensions(List.of(extension)) + .build(); + + var markdown = "# GFM Alerts Demo\n\n" + + "> [!NOTE]\n" + + "> Highlights information that users should take into account.\n\n" + + "> [!TIP]\n" + + "> Helpful advice for doing things better.\n\n" + + "> [!IMPORTANT]\n" + + "> Key information users need to know.\n\n" + + "> [!WARNING]\n" + + "> Urgent info that needs immediate attention.\n\n" + + "> [!CAUTION]\n" + + "> Advises about risks or negative outcomes.\n"; + + var html = renderer.render(parser.parse(markdown)); + + System.out.println("Markdown Input:"); + System.out.println(markdown); + System.out.println("\nHTML Output:"); + System.out.println(html); + } + + private static void customTypesExample() { + System.out.println("CUSTOM ALERT TYPES"); + System.out.println("=".repeat(60)); + + var extension = AlertsExtension.builder() + .addCustomType("BUG", "Known Bug") + .build(); + + var parser = Parser.builder() + .extensions(List.of(extension)) + .build(); + + var renderer = HtmlRenderer.builder() + .extensions(List.of(extension)) + .build(); + + var markdown = "# Custom Alert Types\n\n" + + "> [!NOTE]\n" + + "> Useful information that users should know.\n\n" + + "> [!TIP]\n" + + "> Helpful advice for doing things better.\n\n" + + "> [!BUG]\n" + + "> This feature has a known issue with large files (see #42).\n"; + + var html = renderer.render(parser.parse(markdown)); + + System.out.println("Markdown Input:"); + System.out.println(markdown); + System.out.println("\nHTML Output:"); + System.out.println(html); + } +} diff --git a/commonmark-ext-gfm-alerts/src/test/resources/alerts-spec-template.md b/commonmark-ext-gfm-alerts/src/test/resources/alerts-spec-template.md new file mode 100644 index 000000000..9c1cf117b --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/test/resources/alerts-spec-template.md @@ -0,0 +1,280 @@ +# Alerts + +## Standard types + +```````````````````````````````` example alert +> [!NOTE] +> This is a note +```````````````````````````````` + +```````````````````````````````` example alert +> [!TIP] +> This is a tip +```````````````````````````````` + +```````````````````````````````` example alert +> [!IMPORTANT] +> This is important +```````````````````````````````` + +```````````````````````````````` example alert +> [!WARNING] +> This is a warning +```````````````````````````````` + +```````````````````````````````` example alert +> [!CAUTION] +> This is a caution +```````````````````````````````` + +## Case insensitivity + +Alert type matching is case-insensitive. + +```````````````````````````````` example alert +> [!note] +> Content +```````````````````````````````` + +```````````````````````````````` example alert +> [!Note] +> Content +```````````````````````````````` + +## Alert content + +Marker alone in first paragraph, blank line, then content: + +```````````````````````````````` example alert +> [!NOTE] +> +> Content +```````````````````````````````` + +Multiple paragraphs: + +```````````````````````````````` example alert +> [!NOTE] +> First paragraph +> +> Second paragraph +```````````````````````````````` + +Inline formatting: + +```````````````````````````````` example alert +> [!TIP] +> This is **bold** and *italic* +```````````````````````````````` + +Code block inside alert: + +```````````````````````````````` example alert +> [!TIP] +> Code: +> +> function() { } +> +> End +```````````````````````````````` + +List inside alert: + +```````````````````````````````` example alert +> [!IMPORTANT] +> Items: +> - First item +> - Second item +```````````````````````````````` + +Links inside alert: + +```````````````````````````````` example alert +> [!NOTE] +> Check out [this link](https://example.com) for more info +```````````````````````````````` + +Heading inside alert: + +```````````````````````````````` example alert +> [!IMPORTANT] +> ## Heading +> Content below heading +```````````````````````````````` + +Empty lines in middle of alert: + +```````````````````````````````` example alert +> [!NOTE] +> First +> +> +> After empty lines +```````````````````````````````` + +## Not an alert + +Text after marker on the same line: + +```````````````````````````````` example alert +> [!NOTE] Some text +```````````````````````````````` + +Unknown type: + +```````````````````````````````` example alert +> [!INVALID] +> Some text +```````````````````````````````` + +Unconfigured custom type is not an alert: + +```````````````````````````````` example alert +> [!INFO] +> Should be blockquote +```````````````````````````````` + +Marker with no content: + +```````````````````````````````` example alert +> [!NOTE] +```````````````````````````````` + +Whitespace-only content after marker: + +```````````````````````````````` example alert +> [!TIP] +> +> +```````````````````````````````` + +Extra space inside marker: + +```````````````````````````````` example alert +> [! NOTE] +> Should be blockquote +```````````````````````````````` + +Missing brackets: + +```````````````````````````````` example alert +> !NOTE +> Should be blockquote +```````````````````````````````` + +Missing exclamation mark: + +```````````````````````````````` example alert +> [NOTE] +> Should be blockquote +```````````````````````````````` + +Regular blockquote is not affected: + +```````````````````````````````` example alert +> This is a regular blockquote +```````````````````````````````` + +## Boundaries + +Trailing spaces after marker: + +```````````````````````````````` example alert +> [!NOTE] +> This is a note +```````````````````````````````` + +Trailing tabs after marker: + +```````````````````````````````` example alert +> [!WARNING]→→ +> Be careful +```````````````````````````````` + +Leading spaces before blockquote marker: + +```````````````````````````````` example alert + > [!IMPORTANT] + > Content +```````````````````````````````` + +Blank line after marker ends the blockquote (not an alert): + +```````````````````````````````` example alert +> [!NOTE] + +Some text +```````````````````````````````` + +Alert followed by blockquote: + +```````````````````````````````` example alert +> [!NOTE] +> This is an alert + +> This is a blockquote +```````````````````````````````` + +Adjacent alerts: + +```````````````````````````````` example 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 +```````````````````````````````` + +Nested blockquote inside alert: + +```````````````````````````````` example alert +> [!NOTE] +> This is a note +>> Nested blockquote +```````````````````````````````` + +Alert inside list item stays as blockquote: + +```````````````````````````````` example alert +- > [!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 +```````````````````````````````` + +## Continuation and interruption + +Lazy continuation: + +```````````````````````````````` example 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 +```````````````````````````````` \ No newline at end of file diff --git a/commonmark-ext-gfm-alerts/src/test/resources/alerts-spec.txt b/commonmark-ext-gfm-alerts/src/test/resources/alerts-spec.txt new file mode 100644 index 000000000..6f041fee4 --- /dev/null +++ b/commonmark-ext-gfm-alerts/src/test/resources/alerts-spec.txt @@ -0,0 +1,492 @@ +# Alerts + +Expectations verified against GitHub Markdown API (gh api markdown -f mode=gfm). +Our HTML omits GitHub's SVG icons and uses a `data-alert-type` attribute instead. + +## Standard types + +```````````````````````````````` example alert +> [!NOTE] +> This is a note +. +
+

Note

+

This is a note

+
+```````````````````````````````` + +```````````````````````````````` example alert +> [!TIP] +> This is a tip +. +
+

Tip

+

This is a tip

+
+```````````````````````````````` + +```````````````````````````````` example alert +> [!IMPORTANT] +> This is important +. +
+

Important

+

This is important

+
+```````````````````````````````` + +```````````````````````````````` example alert +> [!WARNING] +> This is a warning +. +
+

Warning

+

This is a warning

+
+```````````````````````````````` + +```````````````````````````````` example alert +> [!CAUTION] +> This is a caution +. +
+

Caution

+

This is a caution

+
+```````````````````````````````` + +## Case insensitivity + +Alert type matching is case-insensitive. + +```````````````````````````````` example alert +> [!note] +> Content +. +
+

Note

+

Content

+
+```````````````````````````````` + +```````````````````````````````` example alert +> [!Note] +> Content +. +
+

Note

+

Content

+
+```````````````````````````````` + +## 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* +. +
+

Tip

+

This is bold and italic

+
+```````````````````````````````` + +Code block inside alert: + +```````````````````````````````` example alert +> [!TIP] +> Code: +> +> function() { } +> +> End +. +
+

Tip

+

Code:

+
function() { }
+
+

End

+
+```````````````````````````````` + +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 +. +
+

Note

+

Check out this link for more info

+
+```````````````````````````````` + +Heading inside alert: + +```````````````````````````````` example alert +> [!IMPORTANT] +> ## Heading +> Content below heading +. +
+

Important

+

Heading

+

Content below heading

+
+```````````````````````````````` + +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/pom.xml b/commonmark-ext-gfm-strikethrough/pom.xml index 1fd1269bf..9d8f55e5f 100644 --- a/commonmark-ext-gfm-strikethrough/pom.xml +++ b/commonmark-ext-gfm-strikethrough/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-gfm-strikethrough 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 index 507fc0a88..c497a4db3 100644 --- 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 @@ -3,11 +3,11 @@ import org.commonmark.Extension; import org.commonmark.parser.Parser; import org.commonmark.renderer.markdown.MarkdownRenderer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class StrikethroughMarkdownRendererTest { @@ -30,6 +30,6 @@ protected String render(String source) { private void assertRoundTrip(String input) { String rendered = render(input); - assertEquals(input, rendered); + 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 index 2ca5471b0..f1199b521 100644 --- 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 @@ -7,30 +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.List; import java.util.Set; -@RunWith(Parameterized.class) +@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(); - private final Example example; + @Parameter + Example example; - public StrikethroughSpecTest(Example example) { - this.example = example; - } - - @Parameters(name = "{0}") - public static List data() { - return ExampleReader.readExampleObjects(TestResources.getGfmSpec(), "strikethrough"); + static List data() { + return ExampleReader.readExamples(TestResources.getGfmSpec(), "strikethrough"); } @Test 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 cb3019957..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 @@ -12,12 +12,12 @@ 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.List; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class StrikethroughTest extends RenderingTestCase { @@ -84,14 +84,14 @@ 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 @@ -104,7 +104,7 @@ public void requireTwoTildesOption() { .build(); Node document = parser.parse("~foo~ ~~bar~~"); - assertEquals("(sub)foo(/sub) /bar/", CONTENT_RENDERER.render(document)); + assertThat(CONTENT_RENDERER.render(document)).isEqualTo("(sub)foo(/sub) /bar/"); } @Test @@ -117,8 +117,7 @@ public void sourceSpans() { Node document = parser.parse("hey ~~there~~\n"); Paragraph block = (Paragraph) document.getFirstChild(); Node strikethrough = block.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 4, 4, 9)), - strikethrough.getSourceSpans()); + assertThat(strikethrough.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 4, 4, 9))); } @Override diff --git a/commonmark-ext-gfm-tables/pom.xml b/commonmark-ext-gfm-tables/pom.xml index d261a1d24..5bd323168 100644 --- a/commonmark-ext-gfm-tables/pom.xml +++ b/commonmark-ext-gfm-tables/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-gfm-tables 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 0dfdd3159..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 @@ -274,17 +274,17 @@ public static class Factory extends AbstractBlockParserFactory { @Override public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { List paragraphLines = matchedBlockParser.getParagraphLines().getLines(); - if (paragraphLines.size() == 1 && Characters.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()); 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); } } } 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 index 16303b58c..85c11206c 100644 --- 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 @@ -3,11 +3,11 @@ import org.commonmark.Extension; import org.commonmark.parser.Parser; import org.commonmark.renderer.markdown.MarkdownRenderer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class TableMarkdownRendererTest { @@ -70,6 +70,6 @@ protected String render(String source) { private void assertRoundTrip(String input) { String rendered = render(input); - assertEquals(input, rendered); + 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 d5ea8c836..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,30 +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.List; import java.util.Set; -@RunWith(Parameterized.class) +@ParameterizedClass +@MethodSource("data") public class TablesSpecTest extends RenderingTestCase { 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 data() { - return ExampleReader.readExampleObjects(TestResources.getGfmSpec(), "table"); + static List data() { + return ExampleReader.readExamples(TestResources.getGfmSpec(), "table"); } @Test diff --git a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTest.java b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTest.java index 57f4a4ae5..3f4b37d54 100644 --- a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTest.java +++ b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTest.java @@ -1,8 +1,7 @@ package org.commonmark.ext.gfm.tables; import org.commonmark.Extension; -import org.commonmark.node.Node; -import org.commonmark.node.SourceSpan; +import org.commonmark.node.*; import org.commonmark.parser.IncludeSourceSpans; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.AttributeProvider; @@ -10,13 +9,13 @@ import org.commonmark.renderer.html.AttributeProviderFactory; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import java.util.*; +import java.util.List; +import java.util.Map; +import java.util.Set; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class TablesTest extends RenderingTestCase { @@ -79,11 +78,6 @@ public void separatorNeedsPipes() { assertRendering("Abc|Def\n|--- ---", "

Abc|Def\n|--- ---

\n"); } - @Test - public void headerMustBeOneLine() { - assertRendering("No\nAbc|Def\n---|---", "

No\nAbc|Def\n---|---

\n"); - } - @Test public void oneHeadNoBody() { assertRendering("Abc|Def\n---|---", "\n" + @@ -702,6 +696,26 @@ public void danglingPipe() { "

|

\n"); } + @Test + public void interruptsParagraph() { + assertRendering("text\n" + + "|a |\n" + + "|---|\n" + + "|b |", "

text

\n" + + "
\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
a
b
\n"); + } + @Test public void attributeProviderIsApplied() { AttributeProviderFactory factory = new AttributeProviderFactory() { @@ -730,7 +744,7 @@ public void setAttributes(Node node, String tagName, Map attribu .extensions(EXTENSIONS) .build(); String rendered = renderer.render(PARSER.parse("Abc|Def\n---|---\n1|2")); - assertThat(rendered, is("\n" + + assertThat(rendered).isEqualTo("
\n" + "\n" + "\n" + "\n" + @@ -743,7 +757,7 @@ public void setAttributes(Node node, String tagName, Map attribu "\n" + "\n" + "\n" + - "
Abc2
\n")); + "\n"); } @Test @@ -766,7 +780,7 @@ public void setAttributes(Node node, String tagName, Map attribu .extensions(EXTENSIONS) .build(); String rendered = renderer.render(PARSER.parse("Abc|Def\n-----|---\n1|2")); - assertThat(rendered, is("\n" + + assertThat(rendered).isEqualTo("
\n" + "\n" + "\n" + "\n" + @@ -779,7 +793,7 @@ public void setAttributes(Node node, String tagName, Map attribu "\n" + "\n" + "\n" + - "
Abc2
\n")); + "\n"); } @Test @@ -791,49 +805,78 @@ public void sourceSpans() { Node document = parser.parse("Abc|Def\n---|---\n|1|2\n 3|four|\n|||\n"); TableBlock block = (TableBlock) document.getFirstChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 7), SourceSpan.of(1, 0, 8, 7), - SourceSpan.of(2, 0, 16, 4), SourceSpan.of(3, 0, 21, 8), SourceSpan.of(4, 0, 30, 3)), - block.getSourceSpans()); + assertThat(block.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 7), SourceSpan.of(1, 0, 8, 7), + SourceSpan.of(2, 0, 16, 4), SourceSpan.of(3, 0, 21, 8), SourceSpan.of(4, 0, 30, 3))); TableHead head = (TableHead) block.getFirstChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 7)), head.getSourceSpans()); + assertThat(head.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 7))); TableRow headRow = (TableRow) head.getFirstChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 7)), headRow.getSourceSpans()); + assertThat(headRow.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 7))); TableCell headRowCell1 = (TableCell) headRow.getFirstChild(); TableCell headRowCell2 = (TableCell) headRow.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 3)), headRowCell1.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 3)), headRowCell1.getFirstChild().getSourceSpans()); - assertEquals(List.of(SourceSpan.of(0, 4, 4, 3)), headRowCell2.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(0, 4, 4, 3)), headRowCell2.getFirstChild().getSourceSpans()); + assertThat(headRowCell1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 3))); + assertThat(headRowCell1.getFirstChild().getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 3))); + assertThat(headRowCell2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 4, 4, 3))); + assertThat(headRowCell2.getFirstChild().getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 4, 4, 3))); TableBody body = (TableBody) block.getLastChild(); - assertEquals(List.of(SourceSpan.of(2, 0, 16, 4), SourceSpan.of(3, 0, 21, 8), SourceSpan.of(4, 0, 30, 3)), body.getSourceSpans()); + assertThat(body.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 0, 16, 4), SourceSpan.of(3, 0, 21, 8), SourceSpan.of(4, 0, 30, 3))); TableRow bodyRow1 = (TableRow) body.getFirstChild(); - assertEquals(List.of(SourceSpan.of(2, 0, 16, 4)), bodyRow1.getSourceSpans()); + assertThat(bodyRow1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 0, 16, 4))); TableCell bodyRow1Cell1 = (TableCell) bodyRow1.getFirstChild(); TableCell bodyRow1Cell2 = (TableCell) bodyRow1.getLastChild(); - assertEquals(List.of(SourceSpan.of(2, 1, 17, 1)), bodyRow1Cell1.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(2, 1, 17, 1)), bodyRow1Cell1.getFirstChild().getSourceSpans()); - assertEquals(List.of(SourceSpan.of(2, 3, 19, 1)), bodyRow1Cell2.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(2, 3, 19, 1)), bodyRow1Cell2.getFirstChild().getSourceSpans()); + assertThat(bodyRow1Cell1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 1, 17, 1))); + assertThat(bodyRow1Cell1.getFirstChild().getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 1, 17, 1))); + assertThat(bodyRow1Cell2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 3, 19, 1))); + assertThat(bodyRow1Cell2.getFirstChild().getSourceSpans()).isEqualTo(List.of(SourceSpan.of(2, 3, 19, 1))); TableRow bodyRow2 = (TableRow) body.getFirstChild().getNext(); - assertEquals(List.of(SourceSpan.of(3, 0, 21, 8)), bodyRow2.getSourceSpans()); + assertThat(bodyRow2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 0, 21, 8))); TableCell bodyRow2Cell1 = (TableCell) bodyRow2.getFirstChild(); TableCell bodyRow2Cell2 = (TableCell) bodyRow2.getLastChild(); - assertEquals(List.of(SourceSpan.of(3, 1, 22, 1)), bodyRow2Cell1.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(3, 1, 22, 1)), bodyRow2Cell1.getFirstChild().getSourceSpans()); - assertEquals(List.of(SourceSpan.of(3, 3, 24, 4)), bodyRow2Cell2.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(3, 3, 24, 4)), bodyRow2Cell2.getFirstChild().getSourceSpans()); + assertThat(bodyRow2Cell1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 1, 22, 1))); + assertThat(bodyRow2Cell1.getFirstChild().getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 1, 22, 1))); + assertThat(bodyRow2Cell2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 3, 24, 4))); + assertThat(bodyRow2Cell2.getFirstChild().getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 3, 24, 4))); TableRow bodyRow3 = (TableRow) body.getLastChild(); - assertEquals(List.of(SourceSpan.of(4, 0, 30, 3)), bodyRow3.getSourceSpans()); + assertThat(bodyRow3.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(4, 0, 30, 3))); TableCell bodyRow3Cell1 = (TableCell) bodyRow3.getFirstChild(); TableCell bodyRow3Cell2 = (TableCell) bodyRow3.getLastChild(); - assertEquals(List.of(), bodyRow3Cell1.getSourceSpans()); - assertEquals(List.of(), bodyRow3Cell2.getSourceSpans()); + assertThat(bodyRow3Cell1.getSourceSpans()).isEqualTo(List.of()); + assertThat(bodyRow3Cell2.getSourceSpans()).isEqualTo(List.of()); + } + + @Test + public void sourceSpansWhenInterrupting() { + var parser = Parser.builder() + .extensions(EXTENSIONS) + .includeSourceSpans(IncludeSourceSpans.BLOCKS_AND_INLINES) + .build(); + var document = parser.parse("a\n" + + "bc\n" + + "|de|\n" + + "|---|\n" + + "|fg|"); + + var paragraph = (Paragraph) document.getFirstChild(); + var text = (Text) paragraph.getFirstChild(); + assertThat(text.getLiteral()).isEqualTo("a"); + assertThat(text.getNext()).isInstanceOf(SoftLineBreak.class); + var text2 = (Text) text.getNext().getNext(); + assertThat(text2.getLiteral()).isEqualTo("bc"); + + assertThat(paragraph.getSourceSpans()).isEqualTo(List.of( + SourceSpan.of(0, 0, 0, 1), + SourceSpan.of(1, 0, 2, 2))); + + var table = (TableBlock) document.getLastChild(); + assertThat(table.getSourceSpans()).isEqualTo(List.of( + SourceSpan.of(2, 0, 5, 4), + SourceSpan.of(3, 0, 10, 5), + SourceSpan.of(4, 0, 16, 4))); } @Override diff --git a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTextContentTest.java b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTextContentTest.java index c5ef8cb5a..966f097fd 100644 --- a/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTextContentTest.java +++ b/commonmark-ext-gfm-tables/src/test/java/org/commonmark/ext/gfm/tables/TablesTextContentTest.java @@ -5,7 +5,7 @@ import org.commonmark.renderer.text.LineBreakRendering; import org.commonmark.renderer.text.TextContentRenderer; import org.commonmark.testutil.Asserts; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; diff --git a/commonmark-ext-heading-anchor/pom.xml b/commonmark-ext-heading-anchor/pom.xml index e4a62b2b3..26d2d19b1 100644 --- a/commonmark-ext-heading-anchor/pom.xml +++ b/commonmark-ext-heading-anchor/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-heading-anchor diff --git a/commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorConfigurationTest.java b/commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorConfigurationTest.java index af2ae13cf..438a3a9bd 100644 --- a/commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorConfigurationTest.java +++ b/commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorConfigurationTest.java @@ -3,12 +3,11 @@ import org.commonmark.Extension; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; public class HeadingAnchorConfigurationTest { @@ -30,25 +29,25 @@ public void testDefaultConfigurationHasNoAdditions() { HtmlRenderer renderer = HtmlRenderer.builder() .extensions(List.of(HeadingAnchorExtension.create())) .build(); - assertThat(doRender(renderer, "# "), equalTo("

\n")); + assertThat(doRender(renderer, "# ")).isEqualTo("

\n"); } @Test public void testDefaultIdWhenNoTextOnHeader() { HtmlRenderer renderer = buildRenderer("defid", "", ""); - assertThat(doRender(renderer, "# "), equalTo("

\n")); + assertThat(doRender(renderer, "# ")).isEqualTo("

\n"); } @Test public void testPrefixAddedToHeader() { HtmlRenderer renderer = buildRenderer("", "pre-", ""); - assertThat(doRender(renderer, "# text"), equalTo("

text

\n")); + assertThat(doRender(renderer, "# text")).isEqualTo("

text

\n"); } @Test public void testSuffixAddedToHeader() { HtmlRenderer renderer = buildRenderer("", "", "-post"); - assertThat(doRender(renderer, "# text"), equalTo("

text

\n")); + assertThat(doRender(renderer, "# text")).isEqualTo("

text

\n"); } private String doRender(HtmlRenderer renderer, String text) { diff --git a/commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorTest.java b/commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorTest.java index 872306bed..3149542e3 100644 --- a/commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorTest.java +++ b/commonmark-ext-heading-anchor/src/test/java/org/commonmark/ext/heading/anchor/HeadingAnchorTest.java @@ -4,7 +4,7 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; diff --git a/commonmark-ext-image-attributes/pom.xml b/commonmark-ext-image-attributes/pom.xml index 55b0dce20..e646bc3fd 100644 --- a/commonmark-ext-image-attributes/pom.xml +++ b/commonmark-ext-image-attributes/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-image-attributes diff --git a/commonmark-ext-image-attributes/src/test/java/org/commonmark/ext/image/attributes/ImageAttributesTest.java b/commonmark-ext-image-attributes/src/test/java/org/commonmark/ext/image/attributes/ImageAttributesTest.java index b863bd4b7..3edf8497e 100644 --- a/commonmark-ext-image-attributes/src/test/java/org/commonmark/ext/image/attributes/ImageAttributesTest.java +++ b/commonmark-ext-image-attributes/src/test/java/org/commonmark/ext/image/attributes/ImageAttributesTest.java @@ -8,12 +8,12 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class ImageAttributesTest extends RenderingTestCase { @@ -131,8 +131,7 @@ public void sourceSpans() { Node document = parser.parse("x{height=3 width=4}\n"); Paragraph block = (Paragraph) document.getFirstChild(); Node text = block.getFirstChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 19)), - text.getSourceSpans()); + assertThat(text.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 19))); } @Override diff --git a/commonmark-ext-ins/pom.xml b/commonmark-ext-ins/pom.xml index 9dbe2cf06..48481c073 100644 --- a/commonmark-ext-ins/pom.xml +++ b/commonmark-ext-ins/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-ins diff --git a/commonmark-ext-ins/src/test/java/org/commonmark/ext/ins/InsMarkdownRendererTest.java b/commonmark-ext-ins/src/test/java/org/commonmark/ext/ins/InsMarkdownRendererTest.java index 28f2cd354..6fc9ead67 100644 --- a/commonmark-ext-ins/src/test/java/org/commonmark/ext/ins/InsMarkdownRendererTest.java +++ b/commonmark-ext-ins/src/test/java/org/commonmark/ext/ins/InsMarkdownRendererTest.java @@ -3,11 +3,11 @@ import org.commonmark.Extension; import org.commonmark.parser.Parser; import org.commonmark.renderer.markdown.MarkdownRenderer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class InsMarkdownRendererTest { @@ -28,6 +28,6 @@ protected String render(String source) { private void assertRoundTrip(String input) { String rendered = render(input); - assertEquals(input, rendered); + assertThat(rendered).isEqualTo(input); } } diff --git a/commonmark-ext-ins/src/test/java/org/commonmark/ext/ins/InsTest.java b/commonmark-ext-ins/src/test/java/org/commonmark/ext/ins/InsTest.java index 4603da60b..a5c91a395 100644 --- a/commonmark-ext-ins/src/test/java/org/commonmark/ext/ins/InsTest.java +++ b/commonmark-ext-ins/src/test/java/org/commonmark/ext/ins/InsTest.java @@ -9,12 +9,12 @@ 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.List; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class InsTest extends RenderingTestCase { @@ -82,14 +82,14 @@ public void insideBlockQuote() { public void delimited() { Node document = PARSER.parse("++foo++"); Ins ins = (Ins) document.getFirstChild().getFirstChild(); - assertEquals("++", ins.getOpeningDelimiter()); - assertEquals("++", ins.getClosingDelimiter()); + assertThat(ins.getOpeningDelimiter()).isEqualTo("++"); + assertThat(ins.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 @@ -102,8 +102,7 @@ public void sourceSpans() { Node document = parser.parse("hey ++there++\n"); Paragraph block = (Paragraph) document.getFirstChild(); Node ins = block.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 4, 4, 9)), - ins.getSourceSpans()); + assertThat(ins.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 4, 4, 9))); } @Override diff --git a/commonmark-ext-task-list-items/pom.xml b/commonmark-ext-task-list-items/pom.xml index 3926c8b5e..4359f8707 100644 --- a/commonmark-ext-task-list-items/pom.xml +++ b/commonmark-ext-task-list-items/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-task-list-items diff --git a/commonmark-ext-task-list-items/src/test/java/org/commonmark/ext/task/list/items/TaskListItemsTest.java b/commonmark-ext-task-list-items/src/test/java/org/commonmark/ext/task/list/items/TaskListItemsTest.java index c13e10bb7..0adc615a7 100644 --- a/commonmark-ext-task-list-items/src/test/java/org/commonmark/ext/task/list/items/TaskListItemsTest.java +++ b/commonmark-ext-task-list-items/src/test/java/org/commonmark/ext/task/list/items/TaskListItemsTest.java @@ -4,7 +4,7 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; diff --git a/commonmark-ext-yaml-front-matter/pom.xml b/commonmark-ext-yaml-front-matter/pom.xml index 9e329e0d3..e6822f771 100644 --- a/commonmark-ext-yaml-front-matter/pom.xml +++ b/commonmark-ext-yaml-front-matter/pom.xml @@ -4,7 +4,7 @@ commonmark-parent org.commonmark - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-ext-yaml-front-matter diff --git a/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java b/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java index f46c11b3c..db17d4a4e 100644 --- a/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java +++ b/commonmark-ext-yaml-front-matter/src/test/java/org/commonmark/ext/front/matter/YamlFrontMatterTest.java @@ -6,14 +6,13 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; import java.util.Map; import java.util.Set; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; public class YamlFrontMatterTest extends RenderingTestCase { private static final Set EXTENSIONS = Set.of(YamlFrontMatterExtension.create()); @@ -31,10 +30,10 @@ public void simpleValue() { Map> data = getFrontMatter(input); - assertEquals(1, data.size()); - assertEquals("hello", data.keySet().iterator().next()); - assertEquals(1, data.get("hello").size()); - assertEquals("world", data.get("hello").get(0)); + assertThat(data).hasSize(1); + assertThat(data.keySet().iterator().next()).isEqualTo("hello"); + assertThat(data.get("hello")).hasSize(1); + assertThat(data.get("hello").get(0)).isEqualTo("world"); assertRendering(input, rendered); } @@ -50,9 +49,9 @@ public void emptyValue() { Map> data = getFrontMatter(input); - assertEquals(1, data.size()); - assertEquals("key", data.keySet().iterator().next()); - assertEquals(0, data.get("key").size()); + assertThat(data).hasSize(1); + assertThat(data.keySet().iterator().next()).isEqualTo("key"); + assertThat(data.get("key")).hasSize(0); assertRendering(input, rendered); } @@ -70,11 +69,11 @@ public void listValues() { Map> data = getFrontMatter(input); - assertEquals(1, data.size()); - assertTrue(data.containsKey("list")); - assertEquals(2, data.get("list").size()); - assertEquals("value1", data.get("list").get(0)); - assertEquals("value2", data.get("list").get(1)); + assertThat(data).hasSize(1); + assertThat(data).containsKey("list"); + assertThat(data.get("list")).hasSize(2); + assertThat(data.get("list").get(0)).isEqualTo("value1"); + assertThat(data.get("list").get(1)).isEqualTo("value2"); assertRendering(input, rendered); } @@ -92,10 +91,10 @@ public void literalValue1() { Map> data = getFrontMatter(input); - assertEquals(1, data.size()); - assertTrue(data.containsKey("literal")); - assertEquals(1, data.get("literal").size()); - assertEquals("hello markdown!\nliteral thing...", data.get("literal").get(0)); + assertThat(data).hasSize(1); + assertThat(data).containsKey("literal"); + assertThat(data.get("literal")).hasSize(1); + assertThat(data.get("literal").get(0)).isEqualTo("hello markdown!\nliteral thing..."); assertRendering(input, rendered); } @@ -112,10 +111,10 @@ public void literalValue2() { Map> data = getFrontMatter(input); - assertEquals(1, data.size()); - assertTrue(data.containsKey("literal")); - assertEquals(1, data.get("literal").size()); - assertEquals("- hello markdown!", data.get("literal").get(0)); + assertThat(data).hasSize(1); + assertThat(data).containsKey("literal"); + assertThat(data.get("literal")).hasSize(1); + assertThat(data.get("literal").get(0)).isEqualTo("- hello markdown!"); assertRendering(input, rendered); } @@ -137,20 +136,20 @@ public void complexValues() { Map> data = getFrontMatter(input); - assertEquals(3, data.size()); + assertThat(data).hasSize(3); - assertTrue(data.containsKey("simple")); - assertEquals(1, data.get("simple").size()); - assertEquals("value", data.get("simple").get(0)); + assertThat(data).containsKey("simple"); + assertThat(data.get("simple")).hasSize(1); + assertThat(data.get("simple").get(0)).isEqualTo("value"); - assertTrue(data.containsKey("literal")); - assertEquals(1, data.get("literal").size()); - assertEquals("hello markdown!\n\nliteral literal", data.get("literal").get(0)); + assertThat(data).containsKey("literal"); + assertThat(data.get("literal")).hasSize(1); + assertThat(data.get("literal").get(0)).isEqualTo("hello markdown!\n\nliteral literal"); - assertTrue(data.containsKey("list")); - assertEquals(2, data.get("list").size()); - assertEquals("value1", data.get("list").get(0)); - assertEquals("value2", data.get("list").get(1)); + assertThat(data).containsKey("list"); + assertThat(data.get("list")).hasSize(2); + assertThat(data.get("list").get(0)).isEqualTo("value1"); + assertThat(data.get("list").get(1)).isEqualTo("value2"); assertRendering(input, rendered); } @@ -164,7 +163,7 @@ public void empty() { Map> data = getFrontMatter(input); - assertTrue(data.isEmpty()); + assertThat(data).isEmpty(); assertRendering(input, rendered); } @@ -180,7 +179,7 @@ public void yamlInParagraph() { Map> data = getFrontMatter(input); - assertTrue(data.isEmpty()); + assertThat(data).isEmpty(); assertRendering(input, rendered); } @@ -195,7 +194,7 @@ public void yamlOnSecondLine() { Map> data = getFrontMatter(input); - assertTrue(data.isEmpty()); + assertThat(data).isEmpty(); assertRendering(input, rendered); } @@ -208,7 +207,7 @@ public void nonMatchedStartTag() { Map> data = getFrontMatter(input); - assertTrue(data.isEmpty()); + assertThat(data).isEmpty(); assertRendering(input, rendered); } @@ -222,7 +221,7 @@ public void inList() { Map> data = getFrontMatter(input); - assertTrue(data.isEmpty()); + assertThat(data).isEmpty(); assertRendering(input, rendered); } @@ -240,9 +239,9 @@ public void visitorIgnoresOtherCustomNodes() { document.accept(visitor); Map> data = visitor.getData(); - assertEquals(1, data.size()); - assertTrue(data.containsKey("hello")); - assertEquals(List.of("world"), data.get("hello")); + assertThat(data).hasSize(1); + assertThat(data).containsKey("hello"); + assertThat(data.get("hello")).isEqualTo(List.of("world")); } @Test @@ -261,9 +260,9 @@ public void nodesCanBeModified() { document.accept(visitor); Map> data = visitor.getData(); - assertEquals(1, data.size()); - assertTrue(data.containsKey("see")); - assertEquals(List.of("you"), data.get("see")); + assertThat(data).hasSize(1); + assertThat(data).containsKey("see"); + assertThat(data.get("see")).isEqualTo(List.of("you")); } @Test @@ -275,10 +274,10 @@ public void dotInKeys() { Map> data = getFrontMatter(input); - assertEquals(1, data.size()); - assertEquals("ms.author", data.keySet().iterator().next()); - assertEquals(1, data.get("ms.author").size()); - assertEquals("author", data.get("ms.author").get(0)); + assertThat(data).hasSize(1); + assertThat(data.keySet().iterator().next()).isEqualTo("ms.author"); + assertThat(data.get("ms.author")).hasSize(1); + assertThat(data.get("ms.author").get(0)).isEqualTo("author"); } @Test @@ -292,9 +291,9 @@ public void singleQuotedLiterals() { Map> data = getFrontMatter(input); - assertEquals(2, data.size()); - assertEquals("It's me", data.get("string").get(0)); - assertEquals("I'm here", data.get("list").get(0)); + assertThat(data).hasSize(2); + assertThat(data.get("string").get(0)).isEqualTo("It's me"); + assertThat(data.get("list").get(0)).isEqualTo("I'm here"); } @Test @@ -308,9 +307,9 @@ public void doubleQuotedLiteral() { Map> data = getFrontMatter(input); - assertEquals(2, data.size()); - assertEquals("backslash: \\ quote: \"", data.get("string").get(0)); - assertEquals("hey", data.get("list").get(0)); + assertThat(data).hasSize(2); + assertThat(data.get("string").get(0)).isEqualTo("backslash: \\ quote: \""); + assertThat(data.get("list").get(0)).isEqualTo("hey"); } @Override diff --git a/commonmark-integration-test/pom.xml b/commonmark-integration-test/pom.xml index c5e572291..7e0048a73 100644 --- a/commonmark-integration-test/pom.xml +++ b/commonmark-integration-test/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-integration-test @@ -20,10 +20,18 @@ org.commonmark commonmark-ext-autolink + + org.commonmark + commonmark-ext-footnotes + org.commonmark commonmark-ext-ins + + org.commonmark + commonmark-ext-gfm-alerts + org.commonmark commonmark-ext-gfm-strikethrough diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/BoundsIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/BoundsIntegrationTest.java index 8ee15164a..f1259b825 100644 --- a/commonmark-integration-test/src/test/java/org/commonmark/integration/BoundsIntegrationTest.java +++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/BoundsIntegrationTest.java @@ -3,39 +3,30 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; 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.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.List; -import static org.junit.Assert.assertNotNull; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests various substrings of the spec examples to check for out of bounds exceptions. */ -@RunWith(Parameterized.class) +@ParameterizedClass +@MethodSource("data") public class BoundsIntegrationTest { private static final Parser PARSER = Parser.builder().build(); - protected final String input; + @Parameter + String input; - public BoundsIntegrationTest(String input) { - this.input = input; - } - - @Parameterized.Parameters(name = "{0}") - public static List data() { - List examples = ExampleReader.readExamples(TestResources.getSpec()); - List data = new ArrayList<>(); - for (Example example : examples) { - data.add(new Object[]{example.getSource()}); - } - return data; + static List data() { + return ExampleReader.readExampleSources(TestResources.getSpec()); } @Test @@ -54,7 +45,7 @@ private void parse(String input) { try { Node parsed = PARSER.parse(input); // Parsing should always return a node - assertNotNull(parsed); + assertThat(parsed).isNotNull(); } catch (Exception e) { throw new AssertionError("Parsing failed, input: " + input, e); } diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/Extensions.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/Extensions.java index ee7ee5290..9090c797f 100644 --- a/commonmark-integration-test/src/test/java/org/commonmark/integration/Extensions.java +++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/Extensions.java @@ -2,7 +2,9 @@ import org.commonmark.Extension; import org.commonmark.ext.autolink.AutolinkExtension; +import org.commonmark.ext.footnotes.FootnotesExtension; import org.commonmark.ext.front.matter.YamlFrontMatterExtension; +import org.commonmark.ext.gfm.alerts.AlertsExtension; import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; import org.commonmark.ext.gfm.tables.TablesExtension; import org.commonmark.ext.image.attributes.ImageAttributesExtension; @@ -15,8 +17,10 @@ public class Extensions { static final List ALL_EXTENSIONS = List.of( AutolinkExtension.create(), + FootnotesExtension.create(), ImageAttributesExtension.create(), InsExtension.create(), + AlertsExtension.create(), StrikethroughExtension.create(), TablesExtension.create(), TaskListItemsExtension.create(), diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/ExtensionsIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/ExtensionsIntegrationTest.java index 64cf60ed9..523154d2c 100644 --- a/commonmark-integration-test/src/test/java/org/commonmark/integration/ExtensionsIntegrationTest.java +++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/ExtensionsIntegrationTest.java @@ -3,7 +3,7 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** * Tests to ensure all extensions work well together. diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/MarkdownRendererIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/MarkdownRendererIntegrationTest.java index f05efe1c3..fe14273ab 100644 --- a/commonmark-integration-test/src/test/java/org/commonmark/integration/MarkdownRendererIntegrationTest.java +++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/MarkdownRendererIntegrationTest.java @@ -10,24 +10,16 @@ import org.commonmark.ext.task.list.items.TaskListItemsExtension; import org.commonmark.parser.Parser; import org.commonmark.renderer.markdown.MarkdownRenderer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class MarkdownRendererIntegrationTest { - private static final List EXTENSIONS = List.of( - AutolinkExtension.create(), - ImageAttributesExtension.create(), - InsExtension.create(), - StrikethroughExtension.create(), - TablesExtension.create(), - TaskListItemsExtension.create(), - YamlFrontMatterExtension.create()); - private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build(); - private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(EXTENSIONS).build(); + private static final Parser PARSER = Parser.builder().extensions(Extensions.ALL_EXTENSIONS).build(); + private static final MarkdownRenderer RENDERER = MarkdownRenderer.builder().extensions(Extensions.ALL_EXTENSIONS).build(); @Test public void testStrikethroughInTable() { @@ -40,6 +32,6 @@ private String render(String source) { private void assertRoundTrip(String input) { String rendered = render(input); - assertEquals(input, rendered); + assertThat(rendered).isEqualTo(input); } } diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/SourceSpanIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/SourceSpanIntegrationTest.java index a0649f537..171cc51b1 100644 --- a/commonmark-integration-test/src/test/java/org/commonmark/integration/SourceSpanIntegrationTest.java +++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/SourceSpanIntegrationTest.java @@ -14,10 +14,6 @@ public class SourceSpanIntegrationTest extends SpecIntegrationTest { .includeSourceSpans(IncludeSourceSpans.BLOCKS) .build(); - public SourceSpanIntegrationTest(Example example) { - super(example); - } - @Override protected String render(String source) { return RENDERER.render(PARSER.parse(source)); diff --git a/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java b/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java index 2b615aa41..07853d402 100644 --- a/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java +++ b/commonmark-integration-test/src/test/java/org/commonmark/integration/SpecIntegrationTest.java @@ -4,7 +4,7 @@ import org.commonmark.parser.Parser; import org.commonmark.testutil.example.Example; import org.commonmark.testutil.SpecTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.*; @@ -20,10 +20,6 @@ public class SpecIntegrationTest extends SpecTestCase { protected static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(Extensions.ALL_EXTENSIONS).percentEncodeUrls(true).build(); protected static final Map OVERRIDDEN_EXAMPLES = getOverriddenExamples(); - public SpecIntegrationTest(Example example) { - super(example); - } - @Test public void testHtmlRendering() { String expectedHtml = OVERRIDDEN_EXAMPLES.get(example.getSource()); diff --git a/commonmark-test-util/pom.xml b/commonmark-test-util/pom.xml index 7fb4881f1..6a9c342cc 100644 --- a/commonmark-test-util/pom.xml +++ b/commonmark-test-util/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-test-util @@ -13,8 +13,12 @@ - junit - junit + org.junit.jupiter + junit-jupiter + + + org.assertj + assertj-core diff --git a/commonmark-test-util/src/main/java/module-info.java b/commonmark-test-util/src/main/java/module-info.java index ef983a513..12980d80a 100644 --- a/commonmark-test-util/src/main/java/module-info.java +++ b/commonmark-test-util/src/main/java/module-info.java @@ -2,5 +2,6 @@ exports org.commonmark.testutil; exports org.commonmark.testutil.example; - requires junit; + requires org.assertj.core; + requires org.junit.jupiter.params; } diff --git a/commonmark-test-util/src/main/java/org/commonmark/testutil/Asserts.java b/commonmark-test-util/src/main/java/org/commonmark/testutil/Asserts.java index 64124b129..971a1b4ea 100644 --- a/commonmark-test-util/src/main/java/org/commonmark/testutil/Asserts.java +++ b/commonmark-test-util/src/main/java/org/commonmark/testutil/Asserts.java @@ -1,13 +1,13 @@ package org.commonmark.testutil; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class Asserts { public static void assertRendering(String source, String expectedRendering, String actualRendering) { // include source for better assertion errors String expected = showTabs(expectedRendering + "\n\n" + source); String actual = showTabs(actualRendering + "\n\n" + source); - assertEquals(expected, actual); + assertThat(actual).isEqualTo(expected); } private static String showTabs(String s) { diff --git a/commonmark-test-util/src/main/java/org/commonmark/testutil/RenderingTestCase.java b/commonmark-test-util/src/main/java/org/commonmark/testutil/RenderingTestCase.java index b585f4604..f7da4c008 100644 --- a/commonmark-test-util/src/main/java/org/commonmark/testutil/RenderingTestCase.java +++ b/commonmark-test-util/src/main/java/org/commonmark/testutil/RenderingTestCase.java @@ -1,5 +1,7 @@ package org.commonmark.testutil; +import static org.assertj.core.api.Assertions.assertThat; + public abstract class RenderingTestCase { protected abstract String render(String source); diff --git a/commonmark-test-util/src/main/java/org/commonmark/testutil/SpecTestCase.java b/commonmark-test-util/src/main/java/org/commonmark/testutil/SpecTestCase.java index 2fb175772..c29a6a69a 100644 --- a/commonmark-test-util/src/main/java/org/commonmark/testutil/SpecTestCase.java +++ b/commonmark-test-util/src/main/java/org/commonmark/testutil/SpecTestCase.java @@ -2,30 +2,22 @@ import org.commonmark.testutil.example.Example; import org.commonmark.testutil.example.ExampleReader; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.util.ArrayList; import java.util.List; -@RunWith(Parameterized.class) +@ParameterizedClass +@MethodSource("data") public abstract class SpecTestCase { - protected final Example example; + @Parameter + protected Example example; - public SpecTestCase(Example example) { - this.example = example; + static List data() { + return ExampleReader.readExamples(TestResources.getSpec()); } - - @Parameters(name = "{0}") - public static List data() { - List examples = ExampleReader.readExamples(TestResources.getSpec()); - List data = new ArrayList<>(); - for (Example example : examples) { - data.add(new Object[]{example}); - } - return data; - } - } diff --git a/commonmark-test-util/src/main/java/org/commonmark/testutil/example/ExampleReader.java b/commonmark-test-util/src/main/java/org/commonmark/testutil/example/ExampleReader.java index 6f5dd6276..d40a10f63 100644 --- a/commonmark-test-util/src/main/java/org/commonmark/testutil/example/ExampleReader.java +++ b/commonmark-test-util/src/main/java/org/commonmark/testutil/example/ExampleReader.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * Reader for files containing examples of CommonMark source and the expected HTML rendering (e.g. spec.txt). @@ -42,15 +43,13 @@ public static List readExamples(URL url) { } } + public static List readExamples(URL url, String info) { + var examples = readExamples(url); + return examples.stream().filter(e -> e.getInfo().contains(info)).collect(Collectors.toList()); + } + public static List readExampleObjects(URL url, String info) { - List examples = readExamples(url); - List data = new ArrayList<>(); - for (Example example : examples) { - if (example.getInfo().contains(info)) { - data.add(new Object[]{example}); - } - } - return data; + return readExamples(url, info).stream().map(e -> new Object[]{e}).collect(Collectors.toList()); } public static List readExampleSources(URL url) { diff --git a/commonmark/pom.xml b/commonmark/pom.xml index 17f3bc747..4e060edaa 100644 --- a/commonmark/pom.xml +++ b/commonmark/pom.xml @@ -4,7 +4,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark diff --git a/commonmark/src/main/java/org/commonmark/internal/BlockStartImpl.java b/commonmark/src/main/java/org/commonmark/internal/BlockStartImpl.java index c7e967d46..516f944b2 100644 --- a/commonmark/src/main/java/org/commonmark/internal/BlockStartImpl.java +++ b/commonmark/src/main/java/org/commonmark/internal/BlockStartImpl.java @@ -9,6 +9,7 @@ public class BlockStartImpl extends BlockStart { private int newIndex = -1; private int newColumn = -1; private boolean replaceActiveBlockParser = false; + private int replaceParagraphLines = 0; public BlockStartImpl(BlockParser... blockParsers) { this.blockParsers = blockParsers; @@ -30,6 +31,10 @@ public boolean isReplaceActiveBlockParser() { return replaceActiveBlockParser; } + int getReplaceParagraphLines() { + return replaceParagraphLines; + } + @Override public BlockStart atIndex(int newIndex) { this.newIndex = newIndex; @@ -48,4 +53,12 @@ public BlockStart replaceActiveBlockParser() { return this; } + @Override + public BlockStart replaceParagraphLines(int lines) { + if (!(lines >= 1)) { + throw new IllegalArgumentException("Lines must be >= 1"); + } + this.replaceParagraphLines = lines; + return this; + } } diff --git a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java index 6059cc51c..07d97296b 100644 --- a/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/DocumentParser.java @@ -76,6 +76,7 @@ public class DocumentParser implements ParserState { private final List linkProcessors; private final Set linkMarkers; private final IncludeSourceSpans includeSourceSpans; + private final int maxOpenBlockParsers; private final DocumentBlockParser documentBlockParser; private final Definitions definitions = new Definitions(); @@ -84,7 +85,8 @@ public class DocumentParser implements ParserState { public DocumentParser(List blockParserFactories, InlineParserFactory inlineParserFactory, List inlineContentParserFactories, List delimiterProcessors, - List linkProcessors, Set linkMarkers, IncludeSourceSpans includeSourceSpans) { + List linkProcessors, Set linkMarkers, + IncludeSourceSpans includeSourceSpans, int maxOpenBlockParsers) { this.blockParserFactories = blockParserFactories; this.inlineParserFactory = inlineParserFactory; this.inlineContentParserFactories = inlineContentParserFactories; @@ -92,6 +94,7 @@ public DocumentParser(List blockParserFactories, InlineParse this.linkProcessors = linkProcessors; this.linkMarkers = linkMarkers; this.includeSourceSpans = includeSourceSpans; + this.maxOpenBlockParsers = maxOpenBlockParsers; this.documentBlockParser = new DocumentBlockParser(); activateBlockParser(new OpenBlockParser(documentBlockParser, 0)); @@ -270,9 +273,15 @@ private void parseLine(String ln, int inputIndex) { } List replacedSourceSpans = null; - if (blockStart.isReplaceActiveBlockParser()) { - Block replacedBlock = prepareActiveBlockParserForReplacement(); - replacedSourceSpans = replacedBlock.getSourceSpans(); + if (blockStart.getReplaceParagraphLines() >= 1 || blockStart.isReplaceActiveBlockParser()) { + var activeBlockParser = getActiveBlockParser(); + if (activeBlockParser instanceof ParagraphParser) { + var paragraphParser = (ParagraphParser) activeBlockParser; + var lines = blockStart.isReplaceActiveBlockParser() ? Integer.MAX_VALUE : blockStart.getReplaceParagraphLines(); + replacedSourceSpans = replaceParagraphLines(lines, paragraphParser); + } else if (blockStart.isReplaceActiveBlockParser()) { + replacedSourceSpans = prepareActiveBlockParserForReplacement(activeBlockParser); + } } for (BlockParser newBlockParser : blockStart.getBlockParsers()) { @@ -455,6 +464,9 @@ private void addSourceSpans() { } private BlockStartImpl findBlockStart(BlockParser blockParser) { + if (openBlockParsers.size() > maxOpenBlockParsers) { + return null; + } MatchedBlockParser matchedBlockParser = new MatchedBlockParserImpl(blockParser); for (BlockParserFactory blockParserFactory : blockParserFactories) { BlockStart result = blockParserFactory.tryStart(this, matchedBlockParser); @@ -498,24 +510,23 @@ private OpenBlockParser deactivateBlockParser() { return openBlockParsers.remove(openBlockParsers.size() - 1); } - private Block prepareActiveBlockParserForReplacement() { - // Note that we don't want to parse inlines, as it's getting replaced. - BlockParser old = deactivateBlockParser().blockParser; + private List replaceParagraphLines(int lines, ParagraphParser paragraphParser) { + // Remove lines from paragraph as the new block is using them. + // If all lines are used, this also unlinks the Paragraph block. + var sourceSpans = paragraphParser.removeLines(lines); + // Close the paragraph block parser, which will finalize it. + closeBlockParsers(1); + return sourceSpans; + } - if (old instanceof ParagraphParser) { - ParagraphParser paragraphParser = (ParagraphParser) old; - // Collect any link reference definitions. Note that replacing the active block parser is done after a - // block parser got the current paragraph content using MatchedBlockParser#getContentString. In case the - // paragraph started with link reference definitions, we parse and strip them before the block parser gets - // the content. We want to keep them. - // If no replacement happens, we collect the definitions as part of finalizing blocks. - addDefinitionsFrom(paragraphParser); - } + private List prepareActiveBlockParserForReplacement(BlockParser blockParser) { + // Note that we don't want to parse inlines here, as it's getting replaced. + deactivateBlockParser(); // Do this so that source positions are calculated, which we will carry over to the replacing block. - old.closeBlock(); - old.getBlock().unlink(); - return old.getBlock(); + blockParser.closeBlock(); + blockParser.getBlock().unlink(); + return blockParser.getBlock().getSourceSpans(); } private Document finalizeAndProcess() { diff --git a/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java b/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java index 3bc9ba5c4..05f070137 100644 --- a/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/HeadingParser.java @@ -60,7 +60,7 @@ public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockPar if (!paragraph.isEmpty()) { return BlockStart.of(new HeadingParser(setextHeadingLevel, paragraph)) .atIndex(line.getContent().length()) - .replaceActiveBlockParser(); + .replaceParagraphLines(paragraph.getLines().size()); } } diff --git a/commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java b/commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java index 44f153e00..44422f421 100644 --- a/commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java +++ b/commonmark/src/main/java/org/commonmark/internal/InlineParserImpl.java @@ -383,14 +383,13 @@ private LinkInfo parseLinkInfo(Bracket opener, Position beforeClose) { // - Collapsed: `[foo][]` (foo is both the text and label) // - Shortcut: `[foo]` (foo is both the text and label) - String text = scanner.getSource(opener.contentPosition, beforeClose).getContent(); - // Starting position is after the closing `]` - Position afterClose = scanner.position(); + var afterClose = scanner.position(); // Maybe an inline link/image var destinationTitle = parseInlineDestinationTitle(scanner); if (destinationTitle != null) { + var text = scanner.getSource(opener.contentPosition, beforeClose).getContent(); return new LinkInfoImpl(opener.markerNode, opener.bracketNode, text, null, destinationTitle.destination, destinationTitle.title, afterClose); } // Not an inline link/image, rewind back to after `]`. @@ -401,7 +400,7 @@ private LinkInfo parseLinkInfo(Bracket opener, Position beforeClose) { // failed to be parsed as an inline link/image before. // See if there's a link label like `[bar]` or `[]` - String label = parseLinkLabel(scanner); + var label = parseLinkLabel(scanner); if (label == null) { // No label, rewind back scanner.setPosition(afterClose); @@ -413,6 +412,7 @@ private LinkInfo parseLinkInfo(Bracket opener, Position beforeClose) { return null; } + var text = scanner.getSource(opener.contentPosition, beforeClose).getContent(); return new LinkInfoImpl(opener.markerNode, opener.bracketNode, text, label, null, null, afterClose); } @@ -440,16 +440,9 @@ private Node wrapBracket(Bracket opener, Node wrapperNode, boolean includeMarker opener.bracketNode.unlink(); removeLastBracket(); - // Links within links are not allowed. We found this link, so there can be no other link around it. + // Links within links are not allowed. We found this link, so there can be no other links around it. if (opener.markerNode == null) { - Bracket bracket = lastBracket; - while (bracket != null) { - if (bracket.markerNode == null) { - // Disallow link opener. It will still get matched, but will not result in a link. - bracket.allowed = false; - } - bracket = bracket.previous; - } + disallowPreviousLinks(); } return wrapperNode; @@ -475,6 +468,15 @@ private Node replaceBracket(Bracket opener, Node node, boolean includeMarker) { n.unlink(); n = next; } + + // Links within links are not allowed. We found this link, so there can be no other links around it. + // Note that this makes any syntax like `[foo]` behave the same as built-in links, which is probably a good + // default (it works for footnotes). It might be useful for a `LinkProcessor` to be able to specify the + // behavior; something we could add to `LinkResult` in the future if requested. + if (opener.markerNode == null || !includeMarker) { + disallowPreviousLinks(); + } + return node; } @@ -489,6 +491,17 @@ private void removeLastBracket() { lastBracket = lastBracket.previous; } + private void disallowPreviousLinks() { + Bracket bracket = lastBracket; + while (bracket != null) { + if (bracket.markerNode == null) { + // Disallow link opener. It will still get matched, but will not result in a link. + bracket.allowed = false; + } + bracket = bracket.previous; + } + } + /** * Try to parse the destination and an optional title for an inline link/image. */ @@ -585,7 +598,9 @@ static String parseLinkLabel(Scanner scanner) { private Node parseLineBreak() { scanner.next(); - if (trailingSpaces >= 2) { + var hard = trailingSpaces >= 2; + trailingSpaces = 0; + if (hard) { return new HardLineBreak(); } else { return new SoftLineBreak(); diff --git a/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java b/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java index b58e669ef..637d3b111 100644 --- a/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/LinkReferenceDefinitionParser.java @@ -10,6 +10,7 @@ import org.commonmark.parser.beta.Scanner; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -102,6 +103,14 @@ State getState() { return state; } + List removeLines(int lines) { + var removedSpans = Collections.unmodifiableList(new ArrayList<>( + sourceSpans.subList(Math.max(sourceSpans.size() - lines, 0), sourceSpans.size()))); + removeLast(lines, paragraphLines); + removeLast(lines, sourceSpans); + return removedSpans; + } + private boolean startDefinition(Scanner scanner) { // Finish any outstanding references now. We don't do this earlier because we need addSourceSpan to have been // called before we do it. @@ -269,6 +278,16 @@ private void finishReference() { title = null; } + private static void removeLast(int n, List list) { + if (n >= list.size()) { + list.clear(); + } else { + for (int i = 0; i < n; i++) { + list.remove(list.size() - 1); + } + } + } + enum State { // Looking for the start of a definition, i.e. `[` START_DEFINITION, diff --git a/commonmark/src/main/java/org/commonmark/internal/ParagraphParser.java b/commonmark/src/main/java/org/commonmark/internal/ParagraphParser.java index 93a2dd593..27eb1e647 100644 --- a/commonmark/src/main/java/org/commonmark/internal/ParagraphParser.java +++ b/commonmark/src/main/java/org/commonmark/internal/ParagraphParser.java @@ -79,4 +79,8 @@ public void parseInlines(InlineParser inlineParser) { public SourceLines getParagraphLines() { return linkReferenceDefinitionParser.getParagraphLines(); } + + public List removeLines(int lines) { + return linkReferenceDefinitionParser.removeLines(lines); + } } diff --git a/commonmark/src/main/java/org/commonmark/internal/renderer/text/BulletListHolder.java b/commonmark/src/main/java/org/commonmark/internal/renderer/text/BulletListHolder.java deleted file mode 100644 index a9271dcdb..000000000 --- a/commonmark/src/main/java/org/commonmark/internal/renderer/text/BulletListHolder.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.commonmark.internal.renderer.text; - -import org.commonmark.node.BulletList; - -public class BulletListHolder extends ListHolder { - private final String marker; - - public BulletListHolder(ListHolder parent, BulletList list) { - super(parent); - marker = list.getMarker(); - } - - public String getMarker() { - return marker; - } -} diff --git a/commonmark/src/main/java/org/commonmark/internal/renderer/text/ListHolder.java b/commonmark/src/main/java/org/commonmark/internal/renderer/text/ListHolder.java deleted file mode 100644 index cb06d4a9d..000000000 --- a/commonmark/src/main/java/org/commonmark/internal/renderer/text/ListHolder.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.commonmark.internal.renderer.text; - -public abstract class ListHolder { - private static final String INDENT_DEFAULT = " "; - private static final String INDENT_EMPTY = ""; - - private final ListHolder parent; - private final String indent; - - ListHolder(ListHolder parent) { - this.parent = parent; - - if (parent != null) { - indent = parent.indent + INDENT_DEFAULT; - } else { - indent = INDENT_EMPTY; - } - } - - public ListHolder getParent() { - return parent; - } - - public String getIndent() { - return indent; - } -} diff --git a/commonmark/src/main/java/org/commonmark/internal/renderer/text/OrderedListHolder.java b/commonmark/src/main/java/org/commonmark/internal/renderer/text/OrderedListHolder.java deleted file mode 100644 index e5e470951..000000000 --- a/commonmark/src/main/java/org/commonmark/internal/renderer/text/OrderedListHolder.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.commonmark.internal.renderer.text; - -import org.commonmark.node.OrderedList; - -public class OrderedListHolder extends ListHolder { - private final String delimiter; - private int counter; - - public OrderedListHolder(ListHolder parent, OrderedList list) { - super(parent); - delimiter = list.getMarkerDelimiter() != null ? list.getMarkerDelimiter() : "."; - counter = list.getMarkerStartNumber() != null ? list.getMarkerStartNumber() : 1; - } - - public String getDelimiter() { - return delimiter; - } - - public int getCounter() { - return counter; - } - - public void increaseCounter() { - counter++; - } -} diff --git a/commonmark/src/main/java/org/commonmark/node/ListItem.java b/commonmark/src/main/java/org/commonmark/node/ListItem.java index e488e8fbe..c4d1214e7 100644 --- a/commonmark/src/main/java/org/commonmark/node/ListItem.java +++ b/commonmark/src/main/java/org/commonmark/node/ListItem.java @@ -2,6 +2,10 @@ /** * A child of a {@link ListBlock}, containing other blocks (e.g. {@link Paragraph}, other lists, etc). + *

+ * Note that a list item can't directly contain {@link Text}, it needs to be: + * {@link ListItem} : {@link Paragraph} : {@link Text}. + * If you want a list that is rendered tightly, create a list with {@link ListBlock#setTight(boolean)}. * * @see CommonMark Spec: List items */ @@ -57,4 +61,18 @@ public Integer getContentIndent() { public void setContentIndent(Integer contentIndent) { this.contentIndent = contentIndent; } + + /** + * @deprecated list items should only contain block nodes; if you're trying to create a list that is rendered + * without paragraphs, use {@link ListBlock#setTight(boolean)} instead. + */ + @Override + @Deprecated + public void appendChild(Node child) { + super.appendChild(child); + } + + public void appendChild(Block child) { + super.appendChild(child); + } } diff --git a/commonmark/src/main/java/org/commonmark/parser/Parser.java b/commonmark/src/main/java/org/commonmark/parser/Parser.java index b98d0581f..8faac789b 100644 --- a/commonmark/src/main/java/org/commonmark/parser/Parser.java +++ b/commonmark/src/main/java/org/commonmark/parser/Parser.java @@ -37,6 +37,7 @@ public class Parser { private final InlineParserFactory inlineParserFactory; private final List postProcessors; private final IncludeSourceSpans includeSourceSpans; + private final int maxOpenBlockParsers; private Parser(Builder builder) { this.blockParserFactories = DocumentParser.calculateBlockParserFactories(builder.blockParserFactories, builder.enabledBlockTypes); @@ -47,6 +48,7 @@ private Parser(Builder builder) { this.linkProcessors = builder.linkProcessors; this.linkMarkers = builder.linkMarkers; this.includeSourceSpans = builder.includeSourceSpans; + this.maxOpenBlockParsers = builder.maxOpenBlockParsers; // Try to construct an inline parser. Invalid configuration might result in an exception, which we want to // detect as soon as possible. @@ -106,7 +108,7 @@ public Node parseReader(Reader input) throws IOException { private DocumentParser createDocumentParser() { return new DocumentParser(blockParserFactories, inlineParserFactory, inlineContentParserFactories, - delimiterProcessors, linkProcessors, linkMarkers, includeSourceSpans); + delimiterProcessors, linkProcessors, linkMarkers, includeSourceSpans, maxOpenBlockParsers); } private Node postProcess(Node document) { @@ -129,6 +131,7 @@ public static class Builder { private Set> enabledBlockTypes = DocumentParser.getDefaultBlockParserTypes(); private InlineParserFactory inlineParserFactory; private IncludeSourceSpans includeSourceSpans = IncludeSourceSpans.NONE; + private int maxOpenBlockParsers = Integer.MAX_VALUE; /** * @return the configured {@link Parser} @@ -200,6 +203,27 @@ public Builder includeSourceSpans(IncludeSourceSpans includeSourceSpans) { return this; } + /** + * Limit how many block parsers may be open at once while parsing. + *

+ * Once the limit is reached, additional block starts are treated as plain text instead of + * creating deeper nested block structure. + *

+ * The document root parser is not counted. The default is unlimited, so callers that keep + * using {@code Parser.builder().build()} preserve behavior. + * + * @param maxOpenBlockParsers maximum number of open non-document block parsers, must be + * zero or greater + * @return {@code this} + */ + public Builder maxOpenBlockParsers(int maxOpenBlockParsers) { + if (maxOpenBlockParsers < 0) { + throw new IllegalArgumentException("maxOpenBlockParsers must be >= 0"); + } + this.maxOpenBlockParsers = maxOpenBlockParsers; + return this; + } + /** * Add a custom block parser factory. *

diff --git a/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java b/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java index d9e7a2b49..c41f1caa3 100644 --- a/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java +++ b/commonmark/src/main/java/org/commonmark/parser/block/BlockStart.java @@ -10,18 +10,59 @@ public abstract class BlockStart { protected BlockStart() { } + /** + * Result for when there is no block start. + */ public static BlockStart none() { return null; } + /** + * Start block(s) with the specified parser(s). + */ public static BlockStart of(BlockParser... blockParsers) { return new BlockStartImpl(blockParsers); } + /** + * Continue parsing at the specified index. + * + * @param newIndex the new index, see {@link ParserState#getIndex()} + */ public abstract BlockStart atIndex(int newIndex); + /** + * Continue parsing at the specified column (for tab handling). + * + * @param newColumn the new column, see {@link ParserState#getColumn()} + */ public abstract BlockStart atColumn(int newColumn); + /** + * @deprecated use {@link #replaceParagraphLines(int)} instead; please raise an issue if that doesn't work for you + * for some reason. + */ + @Deprecated public abstract BlockStart replaceActiveBlockParser(); + /** + * Replace a number of lines from the current paragraph (as returned by + * {@link MatchedBlockParser#getParagraphLines()}) with the new block. + *

+ * This is useful for parsing blocks that start with normal paragraphs and only have special marker syntax in later + * lines, e.g. in this: + *

+     * Foo
+     * ===
+     * 
+ * The Foo line is initially parsed as a normal paragraph, then === is parsed as a heading + * marker, replacing the 1 paragraph line before. The end result is a single Heading block. + *

+ * Note that source spans from the replaced lines are automatically added to the new block. + * + * @param lines the number of lines to replace (at least 1); use {@link Integer#MAX_VALUE} to replace the whole + * paragraph + */ + public abstract BlockStart replaceParagraphLines(int lines); + } diff --git a/commonmark/src/main/java/org/commonmark/parser/block/MatchedBlockParser.java b/commonmark/src/main/java/org/commonmark/parser/block/MatchedBlockParser.java index 1f2bcfb2a..c4619d8c2 100644 --- a/commonmark/src/main/java/org/commonmark/parser/block/MatchedBlockParser.java +++ b/commonmark/src/main/java/org/commonmark/parser/block/MatchedBlockParser.java @@ -12,7 +12,8 @@ public interface MatchedBlockParser { BlockParser getMatchedBlockParser(); /** - * Returns the current paragraph lines if the matched block is a paragraph. + * Returns the current paragraph lines if the matched block is a paragraph. If you want to use some or all of the + * lines for starting a new block instead, use {@link BlockStart#replaceParagraphLines(int)}. * * @return paragraph content or an empty list */ diff --git a/commonmark/src/main/java/org/commonmark/renderer/html/CoreHtmlNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/html/CoreHtmlNodeRenderer.java index 0603aa013..5c536558e 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/html/CoreHtmlNodeRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/html/CoreHtmlNodeRenderer.java @@ -311,6 +311,11 @@ public void visit(Text text) { sb.append(text.getLiteral()); } + @Override + public void visit(Code code) { + sb.append(code.getLiteral()); + } + @Override public void visit(SoftLineBreak softLineBreak) { sb.append('\n'); diff --git a/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java index 32510feaf..5a81676f4 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/markdown/CoreMarkdownNodeRenderer.java @@ -257,7 +257,7 @@ public void visit(ListItem listItem) { throw new IllegalStateException("Unknown list holder type: " + listHolder); } Integer contentIndent = listItem.getContentIndent(); - String spaces = contentIndent != null ? repeat(" ", contentIndent - marker.length()) : " "; + String spaces = contentIndent != null ? repeat(" ", Math.max(contentIndent - marker.length(), 1)) : " "; writer.writePrefix(marker); writer.writePrefix(spaces); writer.pushPrefix(repeat(" ", marker.length() + spaces.length())); diff --git a/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java b/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java index 68b1fbce5..ee564cbdb 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java +++ b/commonmark/src/main/java/org/commonmark/renderer/text/CoreTextContentNodeRenderer.java @@ -1,8 +1,5 @@ package org.commonmark.renderer.text; -import org.commonmark.internal.renderer.text.BulletListHolder; -import org.commonmark.internal.renderer.text.ListHolder; -import org.commonmark.internal.renderer.text.OrderedListHolder; import org.commonmark.node.*; import org.commonmark.renderer.NodeRenderer; @@ -161,19 +158,30 @@ public void visit(Link link) { @Override public void visit(ListItem listItem) { if (listHolder != null && listHolder instanceof OrderedListHolder) { - OrderedListHolder orderedListHolder = (OrderedListHolder) listHolder; - String indent = stripNewlines() ? "" : orderedListHolder.getIndent(); - textContent.write(indent + orderedListHolder.getCounter() + orderedListHolder.getDelimiter() + " "); + var orderedListHolder = (OrderedListHolder) listHolder; + var marker = orderedListHolder.getCounter() + orderedListHolder.getDelimiter(); + var spaces = " "; + textContent.write(marker); + textContent.write(spaces); + textContent.pushPrefix(repeat(" ", marker.length() + spaces.length())); visitChildren(listItem); textContent.block(); + textContent.popPrefix(); orderedListHolder.increaseCounter(); } else if (listHolder != null && listHolder instanceof BulletListHolder) { BulletListHolder bulletListHolder = (BulletListHolder) listHolder; if (!stripNewlines()) { - textContent.write(bulletListHolder.getIndent() + bulletListHolder.getMarker() + " "); + var marker = bulletListHolder.getMarker(); + var spaces = " "; + textContent.write(marker); + textContent.write(spaces); + textContent.pushPrefix(repeat(" ", marker.length() + spaces.length())); } visitChildren(listItem); textContent.block(); + if (!stripNewlines()) { + textContent.popPrefix(); + } } } @@ -268,4 +276,61 @@ private static String stripTrailingNewline(String s) { return s; } } + + // Keep for Android compat (String.repeat only available on Android 12 and later) + private static String repeat(String s, int count) { + var sb = new StringBuilder(s.length() * count); + for (int i = 0; i < count; i++) { + sb.append(s); + } + return sb.toString(); + } + + private static class BulletListHolder extends ListHolder { + private final String marker; + + public BulletListHolder(ListHolder parent, BulletList list) { + super(parent); + marker = list.getMarker(); + } + + public String getMarker() { + return marker; + } + } + + private abstract static class ListHolder { + private final ListHolder parent; + + ListHolder(ListHolder parent) { + this.parent = parent; + } + + public ListHolder getParent() { + return parent; + } + } + + private static class OrderedListHolder extends ListHolder { + private final String delimiter; + private int counter; + + public OrderedListHolder(ListHolder parent, OrderedList list) { + super(parent); + delimiter = list.getMarkerDelimiter() != null ? list.getMarkerDelimiter() : "."; + counter = list.getMarkerStartNumber() != null ? list.getMarkerStartNumber() : 1; + } + + public String getDelimiter() { + return delimiter; + } + + public int getCounter() { + return counter; + } + + public void increaseCounter() { + counter++; + } + } } diff --git a/commonmark/src/main/java/org/commonmark/renderer/text/TextContentWriter.java b/commonmark/src/main/java/org/commonmark/renderer/text/TextContentWriter.java index 2b9f35070..1fb482785 100644 --- a/commonmark/src/main/java/org/commonmark/renderer/text/TextContentWriter.java +++ b/commonmark/src/main/java/org/commonmark/renderer/text/TextContentWriter.java @@ -8,6 +8,7 @@ public class TextContentWriter { private final Appendable buffer; private final LineBreakRendering lineBreakRendering; + private final LinkedList prefixes = new LinkedList<>(); private final LinkedList tight = new LinkedList<>(); private String blockSeparator = null; @@ -36,6 +37,7 @@ public void colon() { public void line() { append('\n'); + writePrefixes(); } public void block() { @@ -61,6 +63,32 @@ public void write(char c) { append(c); } + /** + * Push a prefix onto the top of the stack. All prefixes are written at the beginning of each line, until the + * prefix is popped again. + * + * @param prefix the raw prefix string + */ + public void pushPrefix(String prefix) { + prefixes.addLast(prefix); + } + + /** + * Write a prefix. + * + * @param prefix the raw prefix string to write + */ + public void writePrefix(String prefix) { + write(prefix); + } + + /** + * Remove the last prefix from the top of the stack. + */ + public void popPrefix() { + prefixes.removeLast(); + } + /** * Change whether blocks are tight or loose. Loose is the default where blocks are separated by a blank line. Tight * is where blocks are not separated by a blank line. Tight blocks are used in lists, if there are no blank lines @@ -84,12 +112,26 @@ private boolean isTight() { return !tight.isEmpty() && tight.getLast(); } + private void writePrefixes() { + for (String prefix : prefixes) { + append(prefix); + } + } + /** * If a block separator has been enqueued with {@link #block()} but not yet written, write it now. */ private void flushBlockSeparator() { if (blockSeparator != null) { - append(blockSeparator); + if (blockSeparator.equals("\n") || blockSeparator.equals("\n\n")) { + for (int i = 0; i < blockSeparator.length(); i++) { + var sep = blockSeparator.charAt(i); + append(sep); + writePrefixes(); + } + } else { + append(blockSeparator); + } blockSeparator = null; } } diff --git a/commonmark/src/test/java/org/commonmark/internal/DocumentParserTest.java b/commonmark/src/test/java/org/commonmark/internal/DocumentParserTest.java index 45143b852..a834665ff 100644 --- a/commonmark/src/test/java/org/commonmark/internal/DocumentParserTest.java +++ b/commonmark/src/test/java/org/commonmark/internal/DocumentParserTest.java @@ -2,17 +2,15 @@ import org.commonmark.node.*; import org.commonmark.parser.block.BlockParserFactory; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.HashSet; import java.util.List; import java.util.Set; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; -public class DocumentParserTest { +class DocumentParserTest { private static final List CORE_FACTORIES = List.of( new BlockQuoteParser.Factory(), new HeadingParser.Factory(), @@ -23,28 +21,28 @@ public class DocumentParserTest { new IndentedCodeBlockParser.Factory()); @Test - public void calculateBlockParserFactories_givenAFullListOfAllowedNodes_includesAllCoreFactories() { + void calculateBlockParserFactories_givenAFullListOfAllowedNodes_includesAllCoreFactories() { List customParserFactories = List.of(); var enabledBlockTypes = Set.of(BlockQuote.class, Heading.class, FencedCodeBlock.class, HtmlBlock.class, ThematicBreak.class, ListBlock.class, IndentedCodeBlock.class); List blockParserFactories = DocumentParser.calculateBlockParserFactories(customParserFactories, enabledBlockTypes); - assertThat(blockParserFactories.size(), is(CORE_FACTORIES.size())); + assertThat(blockParserFactories).hasSameSizeAs(CORE_FACTORIES); for (BlockParserFactory factory : CORE_FACTORIES) { - assertTrue(hasInstance(blockParserFactories, factory.getClass())); + assertThat(hasInstance(blockParserFactories, factory.getClass())).isTrue(); } } @Test - public void calculateBlockParserFactories_givenAListOfAllowedNodes_includesAssociatedFactories() { + void calculateBlockParserFactories_givenAListOfAllowedNodes_includesAssociatedFactories() { List customParserFactories = List.of(); Set> nodes = new HashSet<>(); nodes.add(IndentedCodeBlock.class); List blockParserFactories = DocumentParser.calculateBlockParserFactories(customParserFactories, nodes); - assertThat(blockParserFactories.size(), is(1)); - assertTrue(hasInstance(blockParserFactories, IndentedCodeBlockParser.Factory.class)); + assertThat(blockParserFactories).hasSize(1); + assertThat(hasInstance(blockParserFactories, IndentedCodeBlockParser.Factory.class)).isTrue(); } private boolean hasInstance(List blockParserFactories, Class factoryClass) { diff --git a/commonmark/src/test/java/org/commonmark/internal/LinkReferenceDefinitionParserTest.java b/commonmark/src/test/java/org/commonmark/internal/LinkReferenceDefinitionParserTest.java index 3f22adac6..b69ada0e9 100644 --- a/commonmark/src/test/java/org/commonmark/internal/LinkReferenceDefinitionParserTest.java +++ b/commonmark/src/test/java/org/commonmark/internal/LinkReferenceDefinitionParserTest.java @@ -3,82 +3,82 @@ import org.commonmark.internal.LinkReferenceDefinitionParser.State; import org.commonmark.node.LinkReferenceDefinition; import org.commonmark.parser.SourceLine; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -public class LinkReferenceDefinitionParserTest { +class LinkReferenceDefinitionParserTest { private final LinkReferenceDefinitionParser parser = new LinkReferenceDefinitionParser(); @Test - public void testStartLabel() { + void testStartLabel() { assertState("[", State.LABEL, "["); } @Test - public void testStartNoLabel() { + void testStartNoLabel() { // Not a label assertParagraph("a"); // Can not go back to parsing link reference definitions parse("a"); parse("["); - assertEquals(State.PARAGRAPH, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.PARAGRAPH); assertParagraphLines("a\n[", parser); } @Test - public void testEmptyLabel() { + void testEmptyLabel() { assertParagraph("[]: /"); assertParagraph("[ ]: /"); assertParagraph("[ \t\n\u000B\f\r ]: /"); } @Test - public void testLabelColon() { + void testLabelColon() { assertParagraph("[foo] : /"); } @Test - public void testLabel() { + void testLabel() { assertState("[foo]:", State.DESTINATION, "[foo]:"); assertState("[ foo ]:", State.DESTINATION, "[ foo ]:"); } @Test - public void testLabelInvalid() { + void testLabelInvalid() { assertParagraph("[foo[]:"); } @Test - public void testLabelMultiline() { + void testLabelMultiline() { parse("[two"); - assertEquals(State.LABEL, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.LABEL); parse("lines]:"); - assertEquals(State.DESTINATION, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.DESTINATION); parse("/url"); - assertEquals(State.START_TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_TITLE); assertDef(parser.getDefinitions().get(0), "two\nlines", "/url", null); } @Test - public void testLabelStartsWithNewline() { + void testLabelStartsWithNewline() { parse("["); - assertEquals(State.LABEL, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.LABEL); parse("weird]:"); - assertEquals(State.DESTINATION, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.DESTINATION); parse("/url"); - assertEquals(State.START_TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_TITLE); assertDef(parser.getDefinitions().get(0), "\nweird", "/url", null); } @Test - public void testDestination() { + void testDestination() { parse("[foo]: /url"); - assertEquals(State.START_TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_TITLE); assertParagraphLines("", parser); - assertEquals(1, parser.getDefinitions().size()); + assertThat(parser.getDefinitions()).hasSize(1); assertDef(parser.getDefinitions().get(0), "foo", "/url", null); parse("[bar]: "); @@ -86,91 +86,91 @@ public void testDestination() { } @Test - public void testDestinationInvalid() { + void testDestinationInvalid() { assertParagraph("[foo]: "); } @Test - public void testTitle() { + void testTitle() { parse("[foo]: /url 'title'"); - assertEquals(State.START_DEFINITION, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_DEFINITION); assertParagraphLines("", parser); - assertEquals(1, parser.getDefinitions().size()); + assertThat(parser.getDefinitions()).hasSize(1); assertDef(parser.getDefinitions().get(0), "foo", "/url", "title"); } @Test - public void testTitleStartWhitespace() { + void testTitleStartWhitespace() { parse("[foo]: /url"); - assertEquals(State.START_TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_TITLE); assertParagraphLines("", parser); parse(" "); - assertEquals(State.START_DEFINITION, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_DEFINITION); assertParagraphLines(" ", parser); - assertEquals(1, parser.getDefinitions().size()); + assertThat(parser.getDefinitions()).hasSize(1); assertDef(parser.getDefinitions().get(0), "foo", "/url", null); } @Test - public void testTitleMultiline() { + void testTitleMultiline() { parse("[foo]: /url 'two"); - assertEquals(State.TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.TITLE); assertParagraphLines("[foo]: /url 'two", parser); - assertEquals(0, parser.getDefinitions().size()); + assertThat(parser.getDefinitions()).isEmpty(); parse("lines"); - assertEquals(State.TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.TITLE); assertParagraphLines("[foo]: /url 'two\nlines", parser); - assertEquals(0, parser.getDefinitions().size()); + assertThat(parser.getDefinitions()).isEmpty(); parse("'"); - assertEquals(State.START_DEFINITION, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_DEFINITION); assertParagraphLines("", parser); - assertEquals(1, parser.getDefinitions().size()); + assertThat(parser.getDefinitions()).hasSize(1); assertDef(parser.getDefinitions().get(0), "foo", "/url", "two\nlines\n"); } @Test - public void testTitleMultiline2() { + void testTitleMultiline2() { parse("[foo]: /url '"); - assertEquals(State.TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.TITLE); parse("title'"); - assertEquals(State.START_DEFINITION, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_DEFINITION); assertDef(parser.getDefinitions().get(0), "foo", "/url", "\ntitle"); } @Test - public void testTitleMultiline3() { + void testTitleMultiline3() { parse("[foo]: /url"); - assertEquals(State.START_TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_TITLE); // Note that this looks like a valid title until we parse "bad", at which point we need to treat the whole line // as a paragraph line and discard any already parsed title. parse("\"title\" bad"); - assertEquals(State.PARAGRAPH, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.PARAGRAPH); assertDef(parser.getDefinitions().get(0), "foo", "/url", null); } @Test - public void testTitleMultiline4() { + void testTitleMultiline4() { parse("[foo]: /url"); - assertEquals(State.START_TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.START_TITLE); parse("(title"); - assertEquals(State.TITLE, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.TITLE); parse("foo("); - assertEquals(State.PARAGRAPH, parser.getState()); + assertThat(parser.getState()).isEqualTo(State.PARAGRAPH); assertDef(parser.getDefinitions().get(0), "foo", "/url", null); } @Test - public void testTitleInvalid() { + void testTitleInvalid() { assertParagraph("[foo]: /url (invalid("); assertParagraph("[foo]: 'title'"); assertParagraph("[foo]: /url 'title' INVALID"); @@ -188,18 +188,18 @@ private static void assertState(String input, State state, String paragraphConte LinkReferenceDefinitionParser parser = new LinkReferenceDefinitionParser(); // TODO: Should we check things with source spans here? parser.parse(SourceLine.of(input, null)); - assertEquals(state, parser.getState()); + assertThat(parser.getState()).isEqualTo(state); assertParagraphLines(paragraphContent, parser); } private static void assertDef(LinkReferenceDefinition def, String label, String destination, String title) { - assertEquals(label, def.getLabel()); - assertEquals(destination, def.getDestination()); - assertEquals(title, def.getTitle()); + assertThat(def.getLabel()).isEqualTo(label); + assertThat(def.getDestination()).isEqualTo(destination); + assertThat(def.getTitle()).isEqualTo(title); } private static void assertParagraphLines(String expectedContent, LinkReferenceDefinitionParser parser) { String actual = parser.getParagraphLines().getContent(); - assertEquals(expectedContent, actual); + assertThat(actual).isEqualTo(expectedContent); } } diff --git a/commonmark/src/test/java/org/commonmark/internal/util/EscapingTest.java b/commonmark/src/test/java/org/commonmark/internal/util/EscapingTest.java index 9433eb7d0..eb2f1a801 100644 --- a/commonmark/src/test/java/org/commonmark/internal/util/EscapingTest.java +++ b/commonmark/src/test/java/org/commonmark/internal/util/EscapingTest.java @@ -1,21 +1,21 @@ package org.commonmark.internal.util; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -public class EscapingTest { +class EscapingTest { @Test - public void testEscapeHtml() { - assertEquals("nothing to escape", Escaping.escapeHtml("nothing to escape")); - assertEquals("&", Escaping.escapeHtml("&")); - assertEquals("<", Escaping.escapeHtml("<")); - assertEquals(">", Escaping.escapeHtml(">")); - assertEquals(""", Escaping.escapeHtml("\"")); - assertEquals("< start", Escaping.escapeHtml("< start")); - assertEquals("end >", Escaping.escapeHtml("end >")); - assertEquals("< both >", Escaping.escapeHtml("< both >")); - assertEquals("< middle & too >", Escaping.escapeHtml("< middle & too >")); + void testEscapeHtml() { + assertThat(Escaping.escapeHtml("nothing to escape")).isEqualTo("nothing to escape"); + assertThat(Escaping.escapeHtml("&")).isEqualTo("&"); + assertThat(Escaping.escapeHtml("<")).isEqualTo("<"); + assertThat(Escaping.escapeHtml(">")).isEqualTo(">"); + assertThat(Escaping.escapeHtml("\"")).isEqualTo("""); + assertThat(Escaping.escapeHtml("< start")).isEqualTo("< start"); + assertThat(Escaping.escapeHtml("end >")).isEqualTo("end >"); + assertThat(Escaping.escapeHtml("< both >")).isEqualTo("< both >"); + assertThat(Escaping.escapeHtml("< middle & too >")).isEqualTo("< middle & too >"); } } diff --git a/commonmark/src/test/java/org/commonmark/internal/util/LineReaderTest.java b/commonmark/src/test/java/org/commonmark/internal/util/LineReaderTest.java index fc48623a8..b52713846 100644 --- a/commonmark/src/test/java/org/commonmark/internal/util/LineReaderTest.java +++ b/commonmark/src/test/java/org/commonmark/internal/util/LineReaderTest.java @@ -1,6 +1,6 @@ package org.commonmark.internal.util; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.*; import java.nio.charset.StandardCharsets; @@ -9,13 +9,14 @@ import java.util.Objects; import static java.util.stream.Collectors.joining; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.commonmark.internal.util.LineReader.CHAR_BUFFER_SIZE; -import static org.junit.Assert.*; -public class LineReaderTest { +class LineReaderTest { @Test - public void testReadLine() throws IOException { + void testReadLine() throws IOException { assertLines(); assertLines("", "\n"); @@ -48,21 +49,16 @@ public void testReadLine() throws IOException { } @Test - public void testClose() throws IOException { + void testClose() throws IOException { var reader = new InputStreamReader(new ByteArrayInputStream("test".getBytes(StandardCharsets.UTF_8))); var lineReader = new LineReader(reader); lineReader.close(); lineReader.close(); - try { - reader.read(); - fail("Expected read to throw after closing reader"); - } catch (IOException e) { - // Expected - } + assertThatThrownBy(reader::read).isInstanceOf(IOException.class); } private void assertLines(String... s) throws IOException { - assertTrue("Expected parts needs to be even (pairs of content and terminator)", s.length % 2 == 0); + assertThat(s.length).as("Expected parts needs to be even (pairs of content and terminator)").isEven(); var input = Arrays.stream(s).filter(Objects::nonNull).collect(joining("")); assertLines(new StringReader(input), s); @@ -77,8 +73,8 @@ private static void assertLines(Reader reader, String... expectedParts) throws I lines.add(line); lines.add(lineReader.getLineTerminator()); } - assertNull(lineReader.getLineTerminator()); - assertEquals(Arrays.asList(expectedParts), lines); + assertThat(lineReader.getLineTerminator()).isNull(); + assertThat(lines).containsExactly(expectedParts); } } diff --git a/commonmark/src/test/java/org/commonmark/parser/InlineContentParserTest.java b/commonmark/src/test/java/org/commonmark/parser/InlineContentParserTest.java index ed97b6f31..d0f45a6bc 100644 --- a/commonmark/src/test/java/org/commonmark/parser/InlineContentParserTest.java +++ b/commonmark/src/test/java/org/commonmark/parser/InlineContentParserTest.java @@ -1,53 +1,51 @@ package org.commonmark.parser; -import org.commonmark.node.CustomNode; -import org.commonmark.node.Heading; -import org.commonmark.node.Image; -import org.commonmark.node.Text; +import org.commonmark.node.*; import org.commonmark.parser.beta.InlineContentParser; import org.commonmark.parser.beta.InlineContentParserFactory; import org.commonmark.parser.beta.InlineParserState; import org.commonmark.parser.beta.ParsedInline; import org.commonmark.test.Nodes; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -public class InlineContentParserTest { +class InlineContentParserTest { @Test - public void customInlineContentParser() { + void customInlineContentParser() { var parser = Parser.builder().customInlineContentParserFactory(new DollarInlineParser.Factory()).build(); var doc = parser.parse("Test: $hey *there*$ $you$\n\n# Heading $heading$\n"); var inline1 = Nodes.find(doc, DollarInline.class); - assertEquals("hey *there*", inline1.getLiteral()); + assertThat(inline1.getLiteral()).isEqualTo("hey *there*"); var inline2 = (DollarInline) doc.getFirstChild().getLastChild(); - assertEquals("you", inline2.getLiteral()); + assertThat(inline2.getLiteral()).isEqualTo("you"); var heading = Nodes.find(doc, Heading.class); var inline3 = (DollarInline) heading.getLastChild(); - assertEquals("heading", inline3.getLiteral()); + assertThat(inline3.getLiteral()).isEqualTo("heading"); // Parser is created for each inline snippet, which is why the index resets for the second snippet. - assertEquals(0, inline1.getIndex()); - assertEquals(1, inline2.getIndex()); - assertEquals(0, inline3.getIndex()); + assertThat(inline1.getIndex()).isEqualTo(0); + assertThat(inline2.getIndex()).isEqualTo(1); + assertThat(inline3.getIndex()).isEqualTo(0); } @Test - public void bangInlineContentParser() { + void bangInlineContentParser() { // See if using ! for a custom inline content parser works. // ![] is used for images, but if it's not followed by a [, it should be possible to parse it differently. var parser = Parser.builder().customInlineContentParserFactory(new BangInlineParser.Factory()).build(); var doc = parser.parse("![image](url) !notimage"); var image = Nodes.find(doc, Image.class); - assertEquals("url", image.getDestination()); - assertEquals(" ", ((Text) image.getNext()).getLiteral()); - assertEquals(BangInline.class, image.getNext().getNext().getClass()); - assertEquals("notimage", ((Text) image.getNext().getNext().getNext()).getLiteral()); + assertThat(image.getDestination()).isEqualTo("url"); + assertThat(((Text) image.getNext()).getLiteral()).isEqualTo(" "); + // Class + assertThat(image.getNext().getNext()).isInstanceOf(BangInline.class); + assertThat(((Text) image.getNext().getNext().getNext()).getLiteral()).isEqualTo("notimage"); } private static class DollarInline extends CustomNode { diff --git a/commonmark/src/test/java/org/commonmark/parser/beta/LinkProcessorTest.java b/commonmark/src/test/java/org/commonmark/parser/beta/LinkProcessorTest.java index 6209ac9a0..ef8739128 100644 --- a/commonmark/src/test/java/org/commonmark/parser/beta/LinkProcessorTest.java +++ b/commonmark/src/test/java/org/commonmark/parser/beta/LinkProcessorTest.java @@ -4,13 +4,14 @@ import org.commonmark.node.Text; import org.commonmark.parser.Parser; import org.commonmark.test.Nodes; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; + +class LinkProcessorTest { -public class LinkProcessorTest { @Test - public void testLinkMarkerShouldNotBeIncludedByDefault() { + void testLinkMarkerShouldNotBeIncludedByDefault() { // If a link marker is registered but is not processed, the built-in link processor shouldn't consume it. // And I think by default, other processors shouldn't consume it either (by accident). // So requiring processors to opt into including the marker is better than requiring them to opt out, @@ -19,7 +20,7 @@ public void testLinkMarkerShouldNotBeIncludedByDefault() { var parser = Parser.builder().linkMarker('^').build(); var doc = parser.parse("^[test](url)"); var link = Nodes.find(doc, Link.class); - assertEquals("url", link.getDestination()); - assertEquals("^", ((Text) link.getPrevious()).getLiteral()); + assertThat(link.getDestination()).isEqualTo("url"); + assertThat(((Text) link.getPrevious()).getLiteral()).isEqualTo("^"); } } diff --git a/commonmark/src/test/java/org/commonmark/parser/beta/ScannerTest.java b/commonmark/src/test/java/org/commonmark/parser/beta/ScannerTest.java index 14c118ab9..bd74cab0e 100644 --- a/commonmark/src/test/java/org/commonmark/parser/beta/ScannerTest.java +++ b/commonmark/src/test/java/org/commonmark/parser/beta/ScannerTest.java @@ -3,87 +3,87 @@ import org.commonmark.node.SourceSpan; import org.commonmark.parser.SourceLine; import org.commonmark.parser.SourceLines; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; -public class ScannerTest { +class ScannerTest { @Test - public void testNext() { + void testNext() { Scanner scanner = new Scanner(List.of( SourceLine.of("foo bar", null)), 0, 4); - assertEquals('b', scanner.peek()); + assertThat(scanner.peek()).isEqualTo('b'); scanner.next(); - assertEquals('a', scanner.peek()); + assertThat(scanner.peek()).isEqualTo('a'); scanner.next(); - assertEquals('r', scanner.peek()); + assertThat(scanner.peek()).isEqualTo('r'); scanner.next(); - assertEquals('\0', scanner.peek()); + assertThat(scanner.peek()).isEqualTo('\0'); } @Test - public void testMultipleLines() { + void testMultipleLines() { Scanner scanner = new Scanner(List.of( SourceLine.of("ab", null), SourceLine.of("cde", null)), 0, 0); - assertTrue(scanner.hasNext()); - assertEquals('\0', scanner.peekPreviousCodePoint()); - assertEquals('a', scanner.peek()); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo('\0'); + assertThat(scanner.peek()).isEqualTo('a'); scanner.next(); - assertTrue(scanner.hasNext()); - assertEquals('a', scanner.peekPreviousCodePoint()); - assertEquals('b', scanner.peek()); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo('a'); + assertThat(scanner.peek()).isEqualTo('b'); scanner.next(); - assertTrue(scanner.hasNext()); - assertEquals('b', scanner.peekPreviousCodePoint()); - assertEquals('\n', scanner.peek()); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo('b'); + assertThat(scanner.peek()).isEqualTo('\n'); scanner.next(); - assertTrue(scanner.hasNext()); - assertEquals('\n', scanner.peekPreviousCodePoint()); - assertEquals('c', scanner.peek()); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo('\n'); + assertThat(scanner.peek()).isEqualTo('c'); scanner.next(); - assertTrue(scanner.hasNext()); - assertEquals('c', scanner.peekPreviousCodePoint()); - assertEquals('d', scanner.peek()); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo('c'); + assertThat(scanner.peek()).isEqualTo('d'); scanner.next(); - assertTrue(scanner.hasNext()); - assertEquals('d', scanner.peekPreviousCodePoint()); - assertEquals('e', scanner.peek()); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo('d'); + assertThat(scanner.peek()).isEqualTo('e'); scanner.next(); - assertFalse(scanner.hasNext()); - assertEquals('e', scanner.peekPreviousCodePoint()); - assertEquals('\0', scanner.peek()); + assertThat(scanner.hasNext()).isFalse(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo('e'); + assertThat(scanner.peek()).isEqualTo('\0'); } @Test - public void testCodePoints() { + void testCodePoints() { Scanner scanner = new Scanner(List.of(SourceLine.of("\uD83D\uDE0A", null)), 0, 0); - assertTrue(scanner.hasNext()); - assertEquals('\0', scanner.peekPreviousCodePoint()); - assertEquals(128522, scanner.peekCodePoint()); + assertThat(scanner.hasNext()).isTrue(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo('\0'); + assertThat(scanner.peekCodePoint()).isEqualTo(128522); scanner.next(); // This jumps chars, not code points. So jump two here scanner.next(); - assertFalse(scanner.hasNext()); - assertEquals(128522, scanner.peekPreviousCodePoint()); - assertEquals('\0', scanner.peekCodePoint()); + assertThat(scanner.hasNext()).isFalse(); + assertThat(scanner.peekPreviousCodePoint()).isEqualTo(128522); + assertThat(scanner.peekCodePoint()).isEqualTo('\0'); } @Test - public void testTextBetween() { + void testTextBetween() { Scanner scanner = new Scanner(List.of( SourceLine.of("ab", SourceSpan.of(10, 3, 13, 2)), SourceLine.of("cde", SourceSpan.of(11, 4, 20, 3))), @@ -139,20 +139,20 @@ public void testTextBetween() { } private void assertSourceLines(SourceLines sourceLines, String expectedContent, SourceSpan... expectedSourceSpans) { - assertEquals(expectedContent, sourceLines.getContent()); - assertEquals(List.of(expectedSourceSpans), sourceLines.getSourceSpans()); + assertThat(sourceLines.getContent()).isEqualTo(expectedContent); + assertThat(sourceLines.getSourceSpans()).isEqualTo(List.of(expectedSourceSpans)); } @Test - public void nextString() { + void nextString() { Scanner scanner = Scanner.of(SourceLines.of(List.of( SourceLine.of("hey ya", null), SourceLine.of("hi", null)))); - assertFalse(scanner.next("hoy")); - assertTrue(scanner.next("hey")); - assertTrue(scanner.next(' ')); - assertFalse(scanner.next("yo")); - assertTrue(scanner.next("ya")); - assertFalse(scanner.next(" ")); + assertThat(scanner.next("hoy")).isFalse(); + assertThat(scanner.next("hey")).isTrue(); + assertThat(scanner.next(' ')).isTrue(); + assertThat(scanner.next("yo")).isFalse(); + assertThat(scanner.next("ya")).isTrue(); + assertThat(scanner.next(" ")).isFalse(); } } diff --git a/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java b/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java index 36a5b528c..6a468a08e 100644 --- a/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java +++ b/commonmark/src/test/java/org/commonmark/renderer/markdown/MarkdownRendererTest.java @@ -3,15 +3,12 @@ import org.commonmark.node.*; 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.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; +import static org.assertj.core.api.Assertions.assertThat; import static org.commonmark.testutil.Asserts.assertRendering; -import static org.junit.Assert.assertEquals; public class MarkdownRendererTest { @@ -31,7 +28,7 @@ public void testThematicBreaks() { // Apply fallback for null literal ThematicBreak node = new ThematicBreak(); - assertEquals("___", render(node)); + assertThat(render(node)).isEqualTo("___"); } @Test @@ -179,6 +176,13 @@ public void testOrderedListItemsFromAst() { assertRendering("", "2) Test\n", render(doc)); } + @Test + public void testOrderedListItemsWithStartNumberLongerThanLaterNumber() { + var source = "10001.\n20.\n"; + var doc = parse(source); + assertRendering(source, "10001. \n10002. \n", render(doc)); + } + // Inlines @Test @@ -255,7 +259,7 @@ public void testEmphasis() { Emphasis e2 = new Emphasis(); e1.appendChild(e2); e2.appendChild(new Text("hi")); - assertEquals("*_hi_*\n", render(doc)); + assertThat(render(doc)).isEqualTo("*_hi_*\n"); } @Test @@ -332,12 +336,12 @@ public Set getSpecialCharacters() { MarkdownRenderer renderer = MarkdownRenderer.builder().nodeRendererFactory(nodeRendererFactory).build(); String rendered = renderer.render(parse("# Hello")); - assertEquals("# Custom heading\n", rendered); + assertThat(rendered).isEqualTo("# Custom heading\n"); } private void assertRoundTrip(String input) { String rendered = parseAndRender(input); - assertEquals(input, rendered); + assertThat(rendered).isEqualTo(input); } private String parseAndRender(String source) { diff --git a/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java b/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java index 5df2e5c80..3b88df55d 100644 --- a/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java +++ b/commonmark/src/test/java/org/commonmark/renderer/markdown/SpecMarkdownRendererTest.java @@ -6,15 +6,14 @@ import org.commonmark.testutil.TestResources; import org.commonmark.testutil.example.Example; import org.commonmark.testutil.example.ExampleReader; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; /** * Tests Markdown rendering using the examples in the spec like this: @@ -63,9 +62,8 @@ public void testCoverage() { System.out.println(); } - int expectedPassed = 652; - assertTrue("Expected at least " + expectedPassed + " examples to pass but was " + passes.size(), passes.size() >= expectedPassed); - assertEquals(0, fails.size()); + assertThat(passes).hasSizeGreaterThanOrEqualTo(652); + assertThat(fails).isEmpty(); } private static void printCountsBySection(List examples) { diff --git a/commonmark/src/test/java/org/commonmark/test/AbstractVisitorTest.java b/commonmark/src/test/java/org/commonmark/test/AbstractVisitorTest.java index b3b60fa3b..edb6936f4 100644 --- a/commonmark/src/test/java/org/commonmark/test/AbstractVisitorTest.java +++ b/commonmark/src/test/java/org/commonmark/test/AbstractVisitorTest.java @@ -1,10 +1,9 @@ package org.commonmark.test; import org.commonmark.node.*; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; +import static org.assertj.core.api.Assertions.assertThat; public class AbstractVisitorTest { @@ -26,13 +25,13 @@ public void visit(Text text) { assertCode("foo", paragraph.getFirstChild()); assertCode("bar", paragraph.getFirstChild().getNext()); - assertNull(paragraph.getFirstChild().getNext().getNext()); + assertThat(paragraph.getFirstChild().getNext().getNext()).isNull(); assertCode("bar", paragraph.getLastChild()); } private static void assertCode(String expectedLiteral, Node node) { - assertEquals("Expected node to be a Code node: " + node, Code.class, node.getClass()); + assertThat(node).isInstanceOf(Code.class); Code code = (Code) node; - assertEquals(expectedLiteral, code.getLiteral()); + assertThat(code.getLiteral()).isEqualTo(expectedLiteral); } } diff --git a/commonmark/src/test/java/org/commonmark/test/BlockParserFactoryTest.java b/commonmark/src/test/java/org/commonmark/test/BlockParserFactoryTest.java new file mode 100644 index 000000000..b733d7970 --- /dev/null +++ b/commonmark/src/test/java/org/commonmark/test/BlockParserFactoryTest.java @@ -0,0 +1,127 @@ +package org.commonmark.test; + +import org.commonmark.node.*; +import org.commonmark.parser.IncludeSourceSpans; +import org.commonmark.parser.InlineParser; +import org.commonmark.parser.Parser; +import org.commonmark.parser.SourceLines; +import org.commonmark.parser.block.*; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BlockParserFactoryTest { + + @Test + public void customBlockParserFactory() { + var parser = Parser.builder().customBlockParserFactory(new DashBlockParser.Factory()).build(); + + // The dashes would normally be a ThematicBreak + var doc = parser.parse("hey\n\n---\n"); + + assertThat(doc.getFirstChild()).isInstanceOf(Paragraph.class); + assertThat(((Text) doc.getFirstChild().getFirstChild()).getLiteral()).isEqualTo("hey"); + assertThat(doc.getLastChild()).isInstanceOf(DashBlock.class); + } + + @Test + public void replaceActiveBlockParser() { + var parser = Parser.builder() + .customBlockParserFactory(new StarHeadingBlockParser.Factory()) + .includeSourceSpans(IncludeSourceSpans.BLOCKS_AND_INLINES) + .build(); + + var doc = parser.parse("a\nbc\n***\n"); + + var heading = doc.getFirstChild(); + assertThat(heading).isInstanceOf(StarHeading.class); + assertThat(heading.getNext()).isNull(); + var a = heading.getFirstChild(); + assertThat(a).isInstanceOf(Text.class); + assertThat(((Text) a).getLiteral()).isEqualTo("a"); + var bc = a.getNext().getNext(); + assertThat(bc).isInstanceOf(Text.class); + assertThat(((Text) bc).getLiteral()).isEqualTo("bc"); + assertThat(bc.getNext()).isNull(); + + assertThat(heading.getSourceSpans()).isEqualTo(List.of( + SourceSpan.of(0, 0, 0, 1), + SourceSpan.of(1, 0, 2, 2), + SourceSpan.of(2, 0, 5, 3))); + assertThat(a.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 1))); + assertThat(bc.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(1, 0, 2, 2))); + } + + private static class DashBlock extends CustomBlock { + } + + private static class DashBlockParser extends AbstractBlockParser { + + private DashBlock dash = new DashBlock(); + + @Override + public Block getBlock() { + return dash; + } + + @Override + public BlockContinue tryContinue(ParserState parserState) { + return BlockContinue.none(); + } + + static class Factory extends AbstractBlockParserFactory { + + @Override + public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { + if (state.getLine().getContent().equals("---")) { + return BlockStart.of(new DashBlockParser()); + } + return BlockStart.none(); + } + } + } + + private static class StarHeading extends CustomBlock { + } + + private static class StarHeadingBlockParser extends AbstractBlockParser { + + private final SourceLines content; + private final StarHeading heading = new StarHeading(); + + StarHeadingBlockParser(SourceLines content) { + this.content = content; + } + + @Override + public Block getBlock() { + return heading; + } + + @Override + public BlockContinue tryContinue(ParserState parserState) { + return BlockContinue.none(); + } + + @Override + public void parseInlines(InlineParser inlineParser) { + inlineParser.parse(content, heading); + } + + static class Factory extends AbstractBlockParserFactory { + + @Override + public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { + var lines = matchedBlockParser.getParagraphLines(); + if (state.getLine().getContent().toString().startsWith("***")) { + return BlockStart.of(new StarHeadingBlockParser(lines)) + .replaceActiveBlockParser(); + } else { + return BlockStart.none(); + } + } + } + } +} diff --git a/commonmark/src/test/java/org/commonmark/test/CoreRenderingTestCase.java b/commonmark/src/test/java/org/commonmark/test/CoreRenderingTestCase.java index 38f319e1c..2303d2617 100644 --- a/commonmark/src/test/java/org/commonmark/test/CoreRenderingTestCase.java +++ b/commonmark/src/test/java/org/commonmark/test/CoreRenderingTestCase.java @@ -11,6 +11,7 @@ public class CoreRenderingTestCase extends RenderingTestCase { @Override protected String render(String source) { - return RENDERER.render(PARSER.parse(source)); + var node = PARSER.parse(source); + return RENDERER.render(node); } } diff --git a/commonmark/src/test/java/org/commonmark/test/DelimitedTest.java b/commonmark/src/test/java/org/commonmark/test/DelimitedTest.java index a34a32c44..3f2f0d611 100644 --- a/commonmark/src/test/java/org/commonmark/test/DelimitedTest.java +++ b/commonmark/src/test/java/org/commonmark/test/DelimitedTest.java @@ -2,12 +2,12 @@ import org.commonmark.node.*; import org.commonmark.parser.Parser; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class DelimitedTest { @@ -35,20 +35,20 @@ public void visit(StrongEmphasis node) { }; document.accept(visitor); - assertEquals(4, list.size()); + assertThat(list).hasSize(4); Delimited emphasis = list.get(0); Delimited strong = list.get(1); Delimited important = list.get(2); Delimited critical = list.get(3); - assertEquals("*", emphasis.getOpeningDelimiter()); - assertEquals("*", emphasis.getClosingDelimiter()); - assertEquals("**", strong.getOpeningDelimiter()); - assertEquals("**", strong.getClosingDelimiter()); - assertEquals("_", important.getOpeningDelimiter()); - assertEquals("_", important.getClosingDelimiter()); - assertEquals("__", critical.getOpeningDelimiter()); - assertEquals("__", critical.getClosingDelimiter()); + assertThat(emphasis.getOpeningDelimiter()).isEqualTo("*"); + assertThat(emphasis.getClosingDelimiter()).isEqualTo("*"); + assertThat(strong.getOpeningDelimiter()).isEqualTo("**"); + assertThat(strong.getClosingDelimiter()).isEqualTo("**"); + assertThat(important.getOpeningDelimiter()).isEqualTo("_"); + assertThat(important.getClosingDelimiter()).isEqualTo("_"); + assertThat(critical.getOpeningDelimiter()).isEqualTo("__"); + assertThat(critical.getClosingDelimiter()).isEqualTo("__"); } } diff --git a/commonmark/src/test/java/org/commonmark/test/DelimiterProcessorTest.java b/commonmark/src/test/java/org/commonmark/test/DelimiterProcessorTest.java index 680c40bf2..e4920120d 100644 --- a/commonmark/src/test/java/org/commonmark/test/DelimiterProcessorTest.java +++ b/commonmark/src/test/java/org/commonmark/test/DelimiterProcessorTest.java @@ -11,12 +11,13 @@ import org.commonmark.renderer.html.HtmlNodeRendererFactory; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Locale; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class DelimiterProcessorTest extends RenderingTestCase { @@ -29,8 +30,8 @@ public void delimiterProcessorWithInvalidDelimiterUse() { .customDelimiterProcessor(new CustomDelimiterProcessor(':', 0)) .customDelimiterProcessor(new CustomDelimiterProcessor(';', -1)) .build(); - assertEquals("

:test:

\n", RENDERER.render(parser.parse(":test:"))); - assertEquals("

;test;

\n", RENDERER.render(parser.parse(";test;"))); + assertThat(RENDERER.render(parser.parse(":test:"))).isEqualTo("

:test:

\n"); + assertThat(RENDERER.render(parser.parse(";test;"))).isEqualTo("

;test;

\n"); } @Test @@ -54,16 +55,17 @@ public void multipleDelimitersWithDifferentLengths() { .customDelimiterProcessor(new OneDelimiterProcessor()) .customDelimiterProcessor(new TwoDelimiterProcessor()) .build(); - assertEquals("

(1)one(/1) (2)two(/2)

\n", RENDERER.render(parser.parse("+one+ ++two++"))); - assertEquals("

(1)(2)both(/2)(/1)

\n", RENDERER.render(parser.parse("+++both+++"))); + assertThat(RENDERER.render(parser.parse("+one+ ++two++"))).isEqualTo("

(1)one(/1) (2)two(/2)

\n"); + assertThat(RENDERER.render(parser.parse("+++both+++"))).isEqualTo("

(1)(2)both(/2)(/1)

\n"); } - @Test(expected = IllegalArgumentException.class) + @Test public void multipleDelimitersWithSameLengthConflict() { - Parser.builder() - .customDelimiterProcessor(new OneDelimiterProcessor()) - .customDelimiterProcessor(new OneDelimiterProcessor()) - .build(); + assertThatThrownBy(() -> + Parser.builder() + .customDelimiterProcessor(new OneDelimiterProcessor()) + .customDelimiterProcessor(new OneDelimiterProcessor()) + .build()).isInstanceOf(IllegalArgumentException.class); } @Override diff --git a/commonmark/src/test/java/org/commonmark/test/FencedCodeBlockParserTest.java b/commonmark/src/test/java/org/commonmark/test/FencedCodeBlockParserTest.java index 774c6ff0e..443b0fa51 100644 --- a/commonmark/src/test/java/org/commonmark/test/FencedCodeBlockParserTest.java +++ b/commonmark/src/test/java/org/commonmark/test/FencedCodeBlockParserTest.java @@ -5,9 +5,9 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class FencedCodeBlockParserTest extends RenderingTestCase { @@ -18,8 +18,8 @@ public class FencedCodeBlockParserTest extends RenderingTestCase { public void backtickInfo() { Node document = PARSER.parse("```info ~ test\ncode\n```"); FencedCodeBlock codeBlock = (FencedCodeBlock) document.getFirstChild(); - assertEquals("info ~ test", codeBlock.getInfo()); - assertEquals("code\n", codeBlock.getLiteral()); + assertThat(codeBlock.getInfo()).isEqualTo("info ~ test"); + assertThat(codeBlock.getLiteral()).isEqualTo("code\n"); } @Test diff --git a/commonmark/src/test/java/org/commonmark/test/HeadingParserTest.java b/commonmark/src/test/java/org/commonmark/test/HeadingParserTest.java index a5b179a81..f7bf35a4c 100644 --- a/commonmark/src/test/java/org/commonmark/test/HeadingParserTest.java +++ b/commonmark/src/test/java/org/commonmark/test/HeadingParserTest.java @@ -3,7 +3,7 @@ import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.RenderingTestCase; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class HeadingParserTest extends RenderingTestCase { diff --git a/commonmark/src/test/java/org/commonmark/test/HtmlInlineParserTest.java b/commonmark/src/test/java/org/commonmark/test/HtmlInlineParserTest.java index 965a2f181..8e1fd9790 100644 --- a/commonmark/src/test/java/org/commonmark/test/HtmlInlineParserTest.java +++ b/commonmark/src/test/java/org/commonmark/test/HtmlInlineParserTest.java @@ -1,6 +1,6 @@ package org.commonmark.test; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class HtmlInlineParserTest extends CoreRenderingTestCase { diff --git a/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java b/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java index 8e5a8f30d..02d970949 100644 --- a/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java +++ b/commonmark/src/test/java/org/commonmark/test/HtmlRendererTest.java @@ -5,59 +5,54 @@ import org.commonmark.renderer.NodeRenderer; import org.commonmark.renderer.html.*; import org.commonmark.testutil.TestResources; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class HtmlRendererTest { @Test public void htmlAllowingShouldNotEscapeInlineHtml() { String rendered = htmlAllowingRenderer().render(parse("paragraph with inline & html")); - assertEquals("

paragraph with inline & html

\n", rendered); + assertThat(rendered).isEqualTo("

paragraph with inline & html

\n"); } @Test public void htmlAllowingShouldNotEscapeBlockHtml() { String rendered = htmlAllowingRenderer().render(parse("
block &
")); - assertEquals("
block &
\n", rendered); + assertThat(rendered).isEqualTo("
block &
\n"); } @Test public void htmlEscapingShouldEscapeInlineHtml() { String rendered = htmlEscapingRenderer().render(parse("paragraph with inline & html")); // Note that & is not escaped, as it's a normal text node, not part of the inline HTML. - assertEquals("

paragraph with <span id='foo' class="bar">inline & html</span>

\n", rendered); + assertThat(rendered).isEqualTo("

paragraph with <span id='foo' class="bar">inline & html</span>

\n"); } @Test public void htmlEscapingShouldEscapeHtmlBlocks() { String rendered = htmlEscapingRenderer().render(parse("
block &
")); - assertEquals("

<div id='foo' class="bar">block &amp;</div>

\n", rendered); + assertThat(rendered).isEqualTo("

<div id='foo' class="bar">block &amp;</div>

\n"); } @Test public void textEscaping() { String rendered = defaultRenderer().render(parse("escaping: & < > \" '")); - assertEquals("

escaping: & < > " '

\n", rendered); + assertThat(rendered).isEqualTo("

escaping: & < > " '

\n"); } @Test public void characterReferencesWithoutSemicolonsShouldNotBeParsedShouldBeEscaped() { String input = "[example](javascript:alert('XSS'))"; String rendered = defaultRenderer().render(parse(input)); - assertEquals("

example

\n", rendered); + assertThat(rendered).isEqualTo("

example

\n"); } @Test @@ -66,7 +61,7 @@ public void attributeEscaping() { Link link = new Link(); link.setDestination(":"); paragraph.appendChild(link); - assertEquals("

\n", defaultRenderer().render(paragraph)); + assertThat(defaultRenderer().render(paragraph)).isEqualTo("

\n"); } @Test @@ -75,7 +70,7 @@ public void rawUrlsShouldNotFilterDangerousProtocols() { Link link = new Link(); link.setDestination("javascript:alert(5);"); paragraph.appendChild(link); - assertEquals("

\n", rawUrlsRenderer().render(paragraph)); + assertThat(rawUrlsRenderer().render(paragraph)).isEqualTo("

\n"); } @Test @@ -84,13 +79,13 @@ public void sanitizedUrlsShouldSetRelNoFollow() { Link link = new Link(); link.setDestination("/exampleUrl"); paragraph.appendChild(link); - assertEquals("

\n", sanitizeUrlsRenderer().render(paragraph)); + assertThat(sanitizeUrlsRenderer().render(paragraph)).isEqualTo("

\n"); paragraph = new Paragraph(); link = new Link(); link.setDestination("https://google.com"); paragraph.appendChild(link); - assertEquals("

\n", sanitizeUrlsRenderer().render(paragraph)); + assertThat(sanitizeUrlsRenderer().render(paragraph)).isEqualTo("

\n"); } @Test @@ -99,26 +94,26 @@ public void sanitizedUrlsShouldAllowSafeProtocols() { Link link = new Link(); link.setDestination("http://google.com"); paragraph.appendChild(link); - assertEquals("

\n", sanitizeUrlsRenderer().render(paragraph)); + assertThat(sanitizeUrlsRenderer().render(paragraph)).isEqualTo("

\n"); paragraph = new Paragraph(); link = new Link(); link.setDestination("https://google.com"); paragraph.appendChild(link); - assertEquals("

\n", sanitizeUrlsRenderer().render(paragraph)); + assertThat(sanitizeUrlsRenderer().render(paragraph)).isEqualTo("

\n"); paragraph = new Paragraph(); link = new Link(); link.setDestination("mailto:foo@bar.example.com"); paragraph.appendChild(link); - assertEquals("

\n", sanitizeUrlsRenderer().render(paragraph)); + assertThat(sanitizeUrlsRenderer().render(paragraph)).isEqualTo("

\n"); String image = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAAQSURBVBhXY/iPBVBf8P9/AG8TY51nJdgkAAAAAElFTkSuQmCC"; paragraph = new Paragraph(); link = new Link(); link.setDestination(image); paragraph.appendChild(link); - assertEquals("

\n", sanitizeUrlsRenderer().render(paragraph)); + assertThat(sanitizeUrlsRenderer().render(paragraph)).isEqualTo("

\n"); } @Test @@ -127,45 +122,42 @@ public void sanitizedUrlsShouldFilterDangerousProtocols() { Link link = new Link(); link.setDestination("javascript:alert(5);"); paragraph.appendChild(link); - assertEquals("

\n", sanitizeUrlsRenderer().render(paragraph)); + assertThat(sanitizeUrlsRenderer().render(paragraph)).isEqualTo("

\n"); paragraph = new Paragraph(); link = new Link(); link.setDestination("ftp://google.com"); paragraph.appendChild(link); - assertEquals("

\n", sanitizeUrlsRenderer().render(paragraph)); + assertThat(sanitizeUrlsRenderer().render(paragraph)).isEqualTo("

\n"); } @Test public void percentEncodeUrlDisabled() { - assertEquals("

a

\n", defaultRenderer().render(parse("[a](foo&bar)"))); - assertEquals("

a

\n", defaultRenderer().render(parse("[a](ä)"))); - assertEquals("

a

\n", defaultRenderer().render(parse("[a](foo%20bar)"))); + assertThat(defaultRenderer().render(parse("[a](foo&bar)"))).isEqualTo("

a

\n"); + assertThat(defaultRenderer().render(parse("[a](ä)"))).isEqualTo("

a

\n"); + assertThat(defaultRenderer().render(parse("[a](foo%20bar)"))).isEqualTo("

a

\n"); } @Test public void percentEncodeUrl() { // Entities are escaped anyway - assertEquals("

a

\n", percentEncodingRenderer().render(parse("[a](foo&bar)"))); + assertThat(percentEncodingRenderer().render(parse("[a](foo&bar)"))).isEqualTo("

a

\n"); // Existing encoding is preserved - assertEquals("

a

\n", percentEncodingRenderer().render(parse("[a](foo%20bar)"))); - assertEquals("

a

\n", percentEncodingRenderer().render(parse("[a](foo%61)"))); + assertThat(percentEncodingRenderer().render(parse("[a](foo%20bar)"))).isEqualTo("

a

\n"); + assertThat(percentEncodingRenderer().render(parse("[a](foo%61)"))).isEqualTo("

a

\n"); // Invalid encoding is escaped - assertEquals("

a

\n", percentEncodingRenderer().render(parse("[a](foo%)"))); - assertEquals("

a

\n", percentEncodingRenderer().render(parse("[a](foo%a)"))); - assertEquals("

a

\n", percentEncodingRenderer().render(parse("[a](foo%a_)"))); - assertEquals("

a

\n", percentEncodingRenderer().render(parse("[a](foo%xx)"))); + assertThat(percentEncodingRenderer().render(parse("[a](foo%)"))).isEqualTo("

a

\n"); + assertThat(percentEncodingRenderer().render(parse("[a](foo%a)"))).isEqualTo("

a

\n"); + assertThat(percentEncodingRenderer().render(parse("[a](foo%a_)"))).isEqualTo("

a

\n"); + assertThat(percentEncodingRenderer().render(parse("[a](foo%xx)"))).isEqualTo("

a

\n"); // Reserved characters are preserved, except for '[' and ']' - assertEquals("

a

\n", percentEncodingRenderer().render(parse("[a](!*'();:@&=+$,/?#[])"))); + assertThat(percentEncodingRenderer().render(parse("[a](!*'();:@&=+$,/?#[])"))).isEqualTo("

a

\n"); // Unreserved characters are preserved - assertEquals("

a

\n", - percentEncodingRenderer().render(parse("[a](ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~)"))); + assertThat(percentEncodingRenderer().render(parse("[a](ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~)"))).isEqualTo("

a

\n"); // Other characters are percent-encoded (LATIN SMALL LETTER A WITH DIAERESIS) - assertEquals("

a

\n", - percentEncodingRenderer().render(parse("[a](ä)"))); + assertThat(percentEncodingRenderer().render(parse("[a](ä)"))).isEqualTo("

a

\n"); // Other characters are percent-encoded (MUSICAL SYMBOL G CLEF, surrogate pair in UTF-16) - assertEquals("

a

\n", - percentEncodingRenderer().render(parse("[a](\uD834\uDD1E)"))); + assertThat(percentEncodingRenderer().render(parse("[a](\uD834\uDD1E)"))).isEqualTo("

a

\n"); } @Test @@ -192,10 +184,10 @@ public void setAttributes(Node node, String tagName, Map attribu HtmlRenderer renderer = HtmlRenderer.builder().attributeProviderFactory(custom).build(); String rendered = renderer.render(parse("```info\ncontent\n```")); - assertEquals("
content\n
\n", rendered); + assertThat(rendered).isEqualTo("
content\n
\n"); String rendered2 = renderer.render(parse("```evil\"\ncontent\n```")); - assertEquals("
content\n
\n", rendered2); + assertThat(rendered2).isEqualTo("
content\n
\n"); } @Test @@ -217,7 +209,7 @@ public void setAttributes(Node node, String tagName, Map attribu HtmlRenderer renderer = HtmlRenderer.builder().attributeProviderFactory(custom).build(); String rendered = renderer.render(parse("![foo](/url)\n")); - assertEquals("

\n", rendered); + assertThat(rendered).isEqualTo("

\n"); } @Test @@ -240,7 +232,7 @@ public void setAttributes(Node node, String tagName, Map attribu HtmlRenderer renderer = HtmlRenderer.builder().attributeProviderFactory(factory).build(); String rendered = renderer.render(parse("text node")); String secondPass = renderer.render(parse("text node")); - assertEquals(rendered, secondPass); + assertThat(secondPass).isEqualTo(rendered); } @Test @@ -264,30 +256,37 @@ public void render(Node node) { HtmlRenderer renderer = HtmlRenderer.builder().nodeRendererFactory(nodeRendererFactory).build(); String rendered = renderer.render(parse("foo [bar](/url)")); - assertEquals("

foo test

\n", rendered); + assertThat(rendered).isEqualTo("

foo test

\n"); } @Test public void orderedListStartZero() { - assertEquals("
    \n
  1. Test
  2. \n
\n", defaultRenderer().render(parse("0. Test\n"))); + assertThat(defaultRenderer().render(parse("0. Test\n"))).isEqualTo("
    \n
  1. Test
  2. \n
\n"); } @Test public void imageAltTextWithSoftLineBreak() { - assertEquals("

\"foo\nbar\"

\n", - defaultRenderer().render(parse("![foo\nbar](/url)\n"))); + assertThat(defaultRenderer().render(parse("![foo\nbar](/url)\n"))).isEqualTo("

\"foo\nbar\"

\n"); } @Test public void imageAltTextWithHardLineBreak() { - assertEquals("

\"foo\nbar\"

\n", - defaultRenderer().render(parse("![foo \nbar](/url)\n"))); + assertThat(defaultRenderer().render(parse("![foo \nbar](/url)\n"))).isEqualTo("

\"foo\nbar\"

\n"); } @Test public void imageAltTextWithEntities() { - assertEquals("

\"foo

\n", - defaultRenderer().render(parse("![foo ä](/url)\n"))); + assertThat(defaultRenderer().render(parse("![foo ä](/url)\n"))).isEqualTo("

\"foo

\n"); + } + + @Test + public void imageAltTextWithInlines() { + assertThat(defaultRenderer().render(parse("![_foo_ **bar** [link](/url)](/url)\n"))).isEqualTo("

\"foo

\n"); + } + + @Test + public void imageAltTextWithCode() { + assertThat(defaultRenderer().render(parse("![`foo` bar](/url)\n"))).isEqualTo("

\"foo

\n"); } @Test @@ -304,41 +303,35 @@ public void canRenderContentsOfSingleParagraph() { document.appendChild(current); } - assertEquals("Here I have a test link", - defaultRenderer().render(document)); + assertThat(defaultRenderer().render(document)).isEqualTo("Here I have a test link"); } @Test public void omitSingleParagraphP() { var renderer = HtmlRenderer.builder().omitSingleParagraphP(true).build(); - assertEquals("hi there", renderer.render(parse("hi *there*"))); + assertThat(renderer.render(parse("hi *there*"))).isEqualTo("hi there"); } @Test public void threading() throws Exception { - Parser parser = Parser.builder().build(); - String spec = TestResources.readAsString(TestResources.getSpec()); - final Node document = parser.parse(spec); + var parser = Parser.builder().build(); + var spec = TestResources.readAsString(TestResources.getSpec()); + var document = parser.parse(spec); - final HtmlRenderer htmlRenderer = HtmlRenderer.builder().build(); - String expectedRendering = htmlRenderer.render(document); + var htmlRenderer = HtmlRenderer.builder().build(); + var expectedRendering = htmlRenderer.render(document); // Render in parallel using the same HtmlRenderer instance. - List> futures = new ArrayList<>(); - ExecutorService executorService = Executors.newFixedThreadPool(4); + var futures = new ArrayList>(); + var executorService = Executors.newFixedThreadPool(4); for (int i = 0; i < 40; i++) { - Future future = executorService.submit(new Callable() { - @Override - public String call() throws Exception { - return htmlRenderer.render(document); - } - }); + var future = executorService.submit(() -> htmlRenderer.render(document)); futures.add(future); } - for (Future future : futures) { - String rendering = future.get(); - assertThat(rendering, is(expectedRendering)); + for (var future : futures) { + var rendering = future.get(); + assertThat(rendering).isEqualTo(expectedRendering); } } diff --git a/commonmark/src/test/java/org/commonmark/test/InlineParserContextTest.java b/commonmark/src/test/java/org/commonmark/test/InlineParserContextTest.java index 2f2463ccb..c05cac2d2 100644 --- a/commonmark/src/test/java/org/commonmark/test/InlineParserContextTest.java +++ b/commonmark/src/test/java/org/commonmark/test/InlineParserContextTest.java @@ -10,13 +10,13 @@ import org.commonmark.parser.Parser; import org.commonmark.parser.delimiter.DelimiterProcessor; import org.commonmark.renderer.html.HtmlRenderer; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class InlineParserContextTest { @@ -30,10 +30,10 @@ public void labelShouldBeOriginalNotNormalized() { String rendered = HtmlRenderer.builder().build().render(parser.parse(input)); // Lookup should pass original label to context - assertEquals(List.of("FooBarBaz"), inlineParserFactory.lookups); + assertThat(inlineParserFactory.lookups).isEqualTo(List.of("FooBarBaz")); // Context should normalize label for finding reference - assertEquals("

link with special label

\n", rendered); + assertThat(rendered).isEqualTo("

link with special label

\n"); } static class CapturingInlineParserFactory implements InlineParserFactory { diff --git a/commonmark/src/test/java/org/commonmark/test/LinkReferenceDefinitionNodeTest.java b/commonmark/src/test/java/org/commonmark/test/LinkReferenceDefinitionNodeTest.java index 81a71ee69..8410ff028 100644 --- a/commonmark/src/test/java/org/commonmark/test/LinkReferenceDefinitionNodeTest.java +++ b/commonmark/src/test/java/org/commonmark/test/LinkReferenceDefinitionNodeTest.java @@ -2,13 +2,11 @@ import org.commonmark.node.*; import org.commonmark.parser.Parser; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.List; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; public class LinkReferenceDefinitionNodeTest { @@ -17,12 +15,12 @@ public void testDefinitionWithoutParagraph() { Node document = parse("This is a paragraph with a [foo] link.\n\n[foo]: /url 'title'"); List nodes = Nodes.getChildren(document); - assertThat(nodes.size(), is(2)); - assertThat(nodes.get(0), instanceOf(Paragraph.class)); + assertThat(nodes).hasSize(2); + assertThat(nodes.get(0)).isInstanceOf(Paragraph.class); LinkReferenceDefinition definition = assertDef(nodes.get(1), "foo"); - assertThat(definition.getDestination(), is("/url")); - assertThat(definition.getTitle(), is("title")); + assertThat(definition.getDestination()).isEqualTo("/url"); + assertThat(definition.getTitle()).isEqualTo("title"); } @Test @@ -30,10 +28,10 @@ public void testDefinitionWithParagraph() { Node document = parse("[foo]: /url\nThis is a paragraph with a [foo] link."); List nodes = Nodes.getChildren(document); - assertThat(nodes.size(), is(2)); + assertThat(nodes).hasSize(2); // Note that definition is not part of the paragraph, it's a sibling - assertThat(nodes.get(0), instanceOf(LinkReferenceDefinition.class)); - assertThat(nodes.get(1), instanceOf(Paragraph.class)); + assertThat(nodes.get(0)).isInstanceOf(LinkReferenceDefinition.class); + assertThat(nodes.get(1)).isInstanceOf(Paragraph.class); } @Test @@ -41,8 +39,8 @@ public void testMultipleDefinitions() { Node document = parse("This is a paragraph with a [foo] link.\n\n[foo]: /url\n[bar]: /url"); List nodes = Nodes.getChildren(document); - assertThat(nodes.size(), is(3)); - assertThat(nodes.get(0), instanceOf(Paragraph.class)); + assertThat(nodes).hasSize(3); + assertThat(nodes.get(0)).isInstanceOf(Paragraph.class); assertDef(nodes.get(1), "foo"); assertDef(nodes.get(2), "bar"); } @@ -52,14 +50,14 @@ public void testMultipleDefinitionsWithSameLabel() { Node document = parse("This is a paragraph with a [foo] link.\n\n[foo]: /url1\n[foo]: /url2"); List nodes = Nodes.getChildren(document); - assertThat(nodes.size(), is(3)); - assertThat(nodes.get(0), instanceOf(Paragraph.class)); + assertThat(nodes).hasSize(3); + assertThat(nodes.get(0)).isInstanceOf(Paragraph.class); LinkReferenceDefinition def1 = assertDef(nodes.get(1), "foo"); - assertThat(def1.getDestination(), is("/url1")); + assertThat(def1.getDestination()).isEqualTo("/url1"); // When there's multiple definitions with the same label, the first one "wins", as in reference links will use // that. But we still want to preserve the original definitions in the document. LinkReferenceDefinition def2 = assertDef(nodes.get(2), "foo"); - assertThat(def2.getDestination(), is("/url2")); + assertThat(def2.getDestination()).isEqualTo("/url2"); } @Test @@ -67,42 +65,42 @@ public void testDefinitionOfReplacedBlock() { Node document = parse("[foo]: /url\nHeading\n======="); List nodes = Nodes.getChildren(document); - assertThat(nodes.size(), is(2)); + assertThat(nodes).hasSize(2); assertDef(nodes.get(0), "foo"); - assertThat(nodes.get(1), instanceOf(Heading.class)); + assertThat(nodes.get(1)).isInstanceOf(Heading.class); } @Test public void testDefinitionInListItem() { Node document = parse("* [foo]: /url\n [foo]\n"); - assertThat(document.getFirstChild(), instanceOf(BulletList.class)); + assertThat(document.getFirstChild()).isInstanceOf(BulletList.class); Node item = document.getFirstChild().getFirstChild(); - assertThat(item, instanceOf(ListItem.class)); + assertThat(item).isInstanceOf(ListItem.class); List nodes = Nodes.getChildren(item); - assertThat(nodes.size(), is(2)); + assertThat(nodes).hasSize(2); assertDef(nodes.get(0), "foo"); - assertThat(nodes.get(1), instanceOf(Paragraph.class)); + assertThat(nodes.get(1)).isInstanceOf(Paragraph.class); } @Test public void testDefinitionInListItem2() { Node document = parse("* [foo]: /url\n* [foo]\n"); - assertThat(document.getFirstChild(), instanceOf(BulletList.class)); + assertThat(document.getFirstChild()).isInstanceOf(BulletList.class); List items = Nodes.getChildren(document.getFirstChild()); - assertThat(items.size(), is(2)); + assertThat(items).hasSize(2); Node item1 = items.get(0); Node item2 = items.get(1); - assertThat(item1, instanceOf(ListItem.class)); - assertThat(item2, instanceOf(ListItem.class)); + assertThat(item1).isInstanceOf(ListItem.class); + assertThat(item2).isInstanceOf(ListItem.class); - assertThat(Nodes.getChildren(item1).size(), is(1)); + assertThat(Nodes.getChildren(item1)).hasSize(1); assertDef(item1.getFirstChild(), "foo"); - assertThat(Nodes.getChildren(item2).size(), is(1)); - assertThat(item2.getFirstChild(), instanceOf(Paragraph.class)); + assertThat(Nodes.getChildren(item2)).hasSize(1); + assertThat(item2.getFirstChild()).isInstanceOf(Paragraph.class); } @Test @@ -110,8 +108,8 @@ public void testDefinitionLabelCaseIsPreserved() { Node document = parse("This is a paragraph with a [foo] link.\n\n[fOo]: /url 'title'"); List nodes = Nodes.getChildren(document); - assertThat(nodes.size(), is(2)); - assertThat(nodes.get(0), instanceOf(Paragraph.class)); + assertThat(nodes).hasSize(2); + assertThat(nodes.get(0)).isInstanceOf(Paragraph.class); assertDef(nodes.get(1), "fOo"); } @@ -121,9 +119,9 @@ private static Node parse(String input) { } private static LinkReferenceDefinition assertDef(Node node, String label) { - assertThat(node, instanceOf(LinkReferenceDefinition.class)); + assertThat(node).isInstanceOf(LinkReferenceDefinition.class); LinkReferenceDefinition def = (LinkReferenceDefinition) node; - assertThat(def.getLabel(), is(label)); + assertThat(def.getLabel()).isEqualTo(label); return def; } } diff --git a/commonmark/src/test/java/org/commonmark/test/ListBlockParserTest.java b/commonmark/src/test/java/org/commonmark/test/ListBlockParserTest.java index 4a38bc412..02ac3abff 100644 --- a/commonmark/src/test/java/org/commonmark/test/ListBlockParserTest.java +++ b/commonmark/src/test/java/org/commonmark/test/ListBlockParserTest.java @@ -3,9 +3,9 @@ import org.commonmark.node.ListItem; import org.commonmark.node.Node; import org.commonmark.parser.Parser; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class ListBlockParserTest { @@ -59,7 +59,7 @@ public void testOrderedListIndents() { private void assertListItemIndents(String input, int expectedMarkerIndent, int expectedContentIndent) { Node doc = PARSER.parse(input); ListItem listItem = Nodes.find(doc, ListItem.class); - assertEquals(expectedMarkerIndent, (int) listItem.getMarkerIndent()); - assertEquals(expectedContentIndent, (int) listItem.getContentIndent()); + assertThat((int) listItem.getMarkerIndent()).isEqualTo(expectedMarkerIndent); + assertThat((int) listItem.getContentIndent()).isEqualTo(expectedContentIndent); } } diff --git a/commonmark/src/test/java/org/commonmark/test/ListTightLooseTest.java b/commonmark/src/test/java/org/commonmark/test/ListTightLooseTest.java index 4889bb9ab..c6bda31ed 100644 --- a/commonmark/src/test/java/org/commonmark/test/ListTightLooseTest.java +++ b/commonmark/src/test/java/org/commonmark/test/ListTightLooseTest.java @@ -1,6 +1,6 @@ package org.commonmark.test; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class ListTightLooseTest extends CoreRenderingTestCase { diff --git a/commonmark/src/test/java/org/commonmark/test/ParserTest.java b/commonmark/src/test/java/org/commonmark/test/ParserTest.java index 447a50c3d..337196c56 100644 --- a/commonmark/src/test/java/org/commonmark/test/ParserTest.java +++ b/commonmark/src/test/java/org/commonmark/test/ParserTest.java @@ -2,10 +2,10 @@ import org.commonmark.node.*; import org.commonmark.parser.*; -import org.commonmark.parser.block.*; import org.commonmark.renderer.html.HtmlRenderer; +import org.commonmark.renderer.markdown.MarkdownRenderer; import org.commonmark.testutil.TestResources; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; @@ -15,14 +15,11 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class ParserTest { @@ -40,19 +37,7 @@ public void ioReaderTest() throws IOException { Node document2 = parser.parse(spec); HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build(); - assertEquals(renderer.render(document2), renderer.render(document1)); - } - - @Test - public void customBlockParserFactory() { - Parser parser = Parser.builder().customBlockParserFactory(new DashBlockParserFactory()).build(); - - // The dashes would normally be a ThematicBreak - Node document = parser.parse("hey\n\n---\n"); - - assertThat(document.getFirstChild(), instanceOf(Paragraph.class)); - assertEquals("hey", ((Text) document.getFirstChild().getFirstChild()).getLiteral()); - assertThat(document.getLastChild(), instanceOf(DashBlock.class)); + assertThat(renderer.render(document1)).isEqualTo(renderer.render(document2)); } @Test @@ -61,24 +46,25 @@ public void enabledBlockTypes() { Parser parser = Parser.builder().build(); // all core parsers by default Node document = parser.parse(given); - assertThat(document.getFirstChild(), instanceOf(Heading.class)); + assertThat(document.getFirstChild()).isInstanceOf(Heading.class); Set> headersOnly = new HashSet<>(); headersOnly.add(Heading.class); parser = Parser.builder().enabledBlockTypes(headersOnly).build(); document = parser.parse(given); - assertThat(document.getFirstChild(), instanceOf(Heading.class)); + assertThat(document.getFirstChild()).isInstanceOf(Heading.class); Set> noCoreTypes = new HashSet<>(); parser = Parser.builder().enabledBlockTypes(noCoreTypes).build(); document = parser.parse(given); - assertThat(document.getFirstChild(), not(instanceOf(Heading.class))); + assertThat(document.getFirstChild()).isNotInstanceOf(Heading.class); } - @Test(expected = IllegalArgumentException.class) + @Test public void enabledBlockTypesThrowsWhenGivenUnknownClass() { // BulletList can't be enabled separately at the moment, only all ListBlock types - Parser.builder().enabledBlockTypes(Set.of(Heading.class, BulletList.class)).build(); + assertThatThrownBy(() -> + Parser.builder().enabledBlockTypes(Set.of(Heading.class, BulletList.class)).build()).isInstanceOf(IllegalArgumentException.class); } @Test @@ -87,19 +73,19 @@ public void indentation() { Parser parser = Parser.builder().build(); Node document = parser.parse(given); - assertThat(document.getFirstChild(), instanceOf(BulletList.class)); + assertThat(document.getFirstChild()).isInstanceOf(BulletList.class); Node list = document.getFirstChild(); // first level list - assertEquals("expect one child", list.getFirstChild(), list.getLastChild()); - assertEquals("1 space", firstText(list.getFirstChild())); + assertThat(list.getLastChild()).as("expect one child").isEqualTo(list.getFirstChild()); + assertThat(firstText(list.getFirstChild())).isEqualTo("1 space"); list = list.getFirstChild().getLastChild(); // second level list - assertEquals("expect one child", list.getFirstChild(), list.getLastChild()); - assertEquals("3 spaces", firstText(list.getFirstChild())); + assertThat(list.getLastChild()).as("expect one child").isEqualTo(list.getFirstChild()); + assertThat(firstText(list.getFirstChild())).isEqualTo("3 spaces"); list = list.getFirstChild().getLastChild(); // third level list - assertEquals("5 spaces", firstText(list.getFirstChild())); - assertEquals("tab + space", firstText(list.getFirstChild().getNext())); + assertThat(firstText(list.getFirstChild())).isEqualTo("5 spaces"); + assertThat(firstText(list.getFirstChild().getNext())).isEqualTo("tab + space"); } @Test @@ -122,70 +108,146 @@ public InlineParser create(InlineParserContext inlineParserContext) { Parser parser = Parser.builder().inlineParserFactory(fakeInlineParserFactory).build(); String input = "**bold** **bold** ~~strikethrough~~"; - assertThat(parser.parse(input).getFirstChild().getFirstChild(), instanceOf(ThematicBreak.class)); + assertThat(parser.parse(input).getFirstChild().getFirstChild()).isInstanceOf(ThematicBreak.class); } @Test public void threading() throws Exception { - final Parser parser = Parser.builder().build(); - final String spec = TestResources.readAsString(TestResources.getSpec()); + var parser = Parser.builder().build(); + var spec = TestResources.readAsString(TestResources.getSpec()); - HtmlRenderer renderer = HtmlRenderer.builder().build(); - String expectedRendering = renderer.render(parser.parse(spec)); + var renderer = HtmlRenderer.builder().build(); + var expectedRendering = renderer.render(parser.parse(spec)); // Parse in parallel using the same Parser instance. - List> futures = new ArrayList<>(); - ExecutorService executorService = Executors.newFixedThreadPool(4); + var futures = new ArrayList>(); + var executorService = Executors.newFixedThreadPool(4); for (int i = 0; i < 40; i++) { - Future future = executorService.submit(new Callable() { - @Override - public Node call() throws Exception { - return parser.parse(spec); - } - }); + var future = executorService.submit(() -> parser.parse(spec)); futures.add(future); } - for (Future future : futures) { - Node node = future.get(); - assertThat(renderer.render(node), is(expectedRendering)); + for (var future : futures) { + var node = future.get(); + assertThat(renderer.render(node)).isEqualTo(expectedRendering); } } + @Test + public void maxOpenBlockParsersMustBeZeroOrGreater() { + assertThatThrownBy(() -> + Parser.builder().maxOpenBlockParsers(-1)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void maxOpenBlockParsersIsOptIn() { + var parser = Parser.builder().build(); + + var document = parser.parse(alternatingNestedList(9)); + + assertThat(renderText(deepestStructuredParagraph(document, 9))).isEqualTo("level9"); + } + + @Test + public void maxOpenBlockParsersPreservesSevenLogicalListLevelsAtSeventeenBlocks() { + var parser = Parser.builder().maxOpenBlockParsers(17).build(); + + var document = parser.parse(alternatingNestedList(7)); + + assertThat(renderText(deepestStructuredParagraph(document, 7))).isEqualTo("level7"); + } + + @Test + public void maxOpenBlockParsersPreservesEightLogicalListLevelsAtSeventeenBlocks() { + var parser = Parser.builder().maxOpenBlockParsers(17).build(); + + var document = parser.parse(alternatingNestedList(8)); + + assertThat(renderText(deepestStructuredParagraph(document, 8))).isEqualTo("level8"); + } + + @Test + public void maxOpenBlockParsersDegradesTheNinthLogicalListLevelToPlainText() { + var parser = Parser.builder().maxOpenBlockParsers(17).build(); + + var document = parser.parse(alternatingNestedList(9)); + var deepestParagraph = deepestStructuredParagraph(document, 8); + + assertThat(renderText(deepestParagraph)).isEqualTo("level8\n\\- level9"); + assertThat(deepestParagraph.getNext()).isNull(); + } + + @Test + public void maxOpenBlockParsersAlsoLimitsMixedListAndBlockQuoteNesting() { + var parser = Parser.builder().maxOpenBlockParsers(5).build(); + + var document = parser.parse(String.join("\n", + "- level1", + " > level2", + " > > level3", + " > > > level4")); + + var listBlock = document.getFirstChild(); + assertThat(listBlock).isInstanceOf(BulletList.class); + + var listItem = listBlock.getFirstChild(); + var blockQuote1 = listItem.getLastChild(); + assertThat(blockQuote1).isInstanceOf(BlockQuote.class); + + var blockQuote2 = blockQuote1.getLastChild(); + assertThat(blockQuote2).isInstanceOf(BlockQuote.class); + + var deepestParagraph = blockQuote2.getLastChild(); + assertThat(deepestParagraph).isInstanceOf(Paragraph.class); + assertThat(renderText(deepestParagraph)).isEqualTo("level3\n\\> level4"); + assertThat(deepestParagraph.getNext()).isNull(); + } + private String firstText(Node n) { while (!(n instanceof Text)) { - assertThat(n, notNullValue()); + assertThat(n).isNotNull(); n = n.getFirstChild(); } return ((Text) n).getLiteral(); } - private static class DashBlock extends CustomBlock { + private Paragraph deepestStructuredParagraph(Node document, int levels) { + Node node = document.getFirstChild(); + for (int level = 1; level <= levels; level++) { + assertThat(node).isInstanceOf(ListBlock.class); + var listItem = node.getFirstChild(); + assertThat(listItem).isNotNull(); + if (level == levels) { + assertThat(listItem.getFirstChild()).isInstanceOf(Paragraph.class); + return (Paragraph) listItem.getFirstChild(); + } + node = listItem.getLastChild(); + } + throw new AssertionError("unreachable"); } - private static class DashBlockParser extends AbstractBlockParser { - - private DashBlock dash = new DashBlock(); - - @Override - public Block getBlock() { - return dash; - } + private String renderText(Node node) { + return MarkdownRenderer.builder().build().render(node).trim(); + } - @Override - public BlockContinue tryContinue(ParserState parserState) { - return BlockContinue.none(); + private String alternatingNestedList(int levels) { + int indent = 0; + var lines = new ArrayList(); + for (int level = 1; level <= levels; level++) { + var ordered = level % 2 == 0; + var marker = ordered ? "1. " : "- "; + lines.add(" ".repeat(indent) + marker + "level" + level); + indent += marker.length(); } + return String.join("\n", lines); } - private static class DashBlockParserFactory extends AbstractBlockParserFactory { - - @Override - public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { - if (state.getLine().getContent().equals("---")) { - return BlockStart.of(new DashBlockParser()); - } - return BlockStart.none(); + private int depth(Node node) { + int depth = 0; + while (node.getParent() != null) { + node = node.getParent(); + depth++; } + return depth; } } diff --git a/commonmark/src/test/java/org/commonmark/test/PathologicalTest.java b/commonmark/src/test/java/org/commonmark/test/PathologicalTest.java index ae1310ed2..66d39de23 100644 --- a/commonmark/src/test/java/org/commonmark/test/PathologicalTest.java +++ b/commonmark/src/test/java/org/commonmark/test/PathologicalTest.java @@ -1,34 +1,21 @@ package org.commonmark.test; -import org.junit.FixMethodOrder; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.Stopwatch; -import org.junit.rules.Timeout; -import org.junit.runner.Description; -import org.junit.runners.MethodSorters; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.Timeout; import java.util.concurrent.TimeUnit; /** * Pathological input cases (from commonmark.js). */ -@FixMethodOrder(MethodSorters.NAME_ASCENDING) +@Timeout(value = 3, unit = TimeUnit.SECONDS) +@TestMethodOrder(MethodOrderer.MethodName.class) public class PathologicalTest extends CoreRenderingTestCase { private int x = 100_000; - @Rule - public Timeout timeout = new Timeout(3, TimeUnit.SECONDS); - - @Rule - public Stopwatch stopwatch = new Stopwatch() { - @Override - protected void finished(long nanos, Description description) { - System.err.println(description.getDisplayName() + " took " + (nanos / 1000000) + " ms"); - } - }; - @Test public void nestedStrongEmphasis() { // this is limited by the stack size because visitor is recursive diff --git a/commonmark/src/test/java/org/commonmark/test/RegressionTest.java b/commonmark/src/test/java/org/commonmark/test/RegressionTest.java index 94b3a7439..900a6518c 100644 --- a/commonmark/src/test/java/org/commonmark/test/RegressionTest.java +++ b/commonmark/src/test/java/org/commonmark/test/RegressionTest.java @@ -6,18 +6,18 @@ 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.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -@RunWith(Parameterized.class) +@ParameterizedClass +@MethodSource("data") public class RegressionTest extends RenderingTestCase { private static final Parser PARSER = Parser.builder().build(); @@ -26,20 +26,13 @@ public class RegressionTest extends RenderingTestCase { private static final Map OVERRIDDEN_EXAMPLES = getOverriddenExamples(); - private final Example example; + @Parameter + Example example; - public RegressionTest(Example example) { - this.example = example; - } - - @Parameters(name = "{0}") - public static List data() { - List data = new ArrayList<>(); - for (URL regressionResource : TestResources.getRegressions()) { - List examples = ExampleReader.readExamples(regressionResource); - for (Example example : examples) { - data.add(new Object[]{example}); - } + static List data() { + var data = new ArrayList(); + for (var regressionResource : TestResources.getRegressions()) { + data.addAll(ExampleReader.readExamples(regressionResource)); } return data; } diff --git a/commonmark/src/test/java/org/commonmark/test/SourceLineTest.java b/commonmark/src/test/java/org/commonmark/test/SourceLineTest.java index 3fb95d386..5d34bf410 100644 --- a/commonmark/src/test/java/org/commonmark/test/SourceLineTest.java +++ b/commonmark/src/test/java/org/commonmark/test/SourceLineTest.java @@ -2,9 +2,10 @@ import org.commonmark.node.SourceSpan; import org.commonmark.parser.SourceLine; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class SourceLineTest { @@ -25,18 +26,20 @@ public void testSubstring() { assertSourceLine(line.substring(4, 4), "", null); } - @Test(expected = StringIndexOutOfBoundsException.class) + @Test public void testSubstringBeginOutOfBounds() { - SourceLine.of("abcd", SourceSpan.of(3, 10, 13, 4)).substring(3, 2); + var sourceLine = SourceLine.of("abcd", SourceSpan.of(3, 10, 13, 4)); + assertThatThrownBy(() -> sourceLine.substring(3, 2)).isInstanceOf(StringIndexOutOfBoundsException.class); } - @Test(expected = StringIndexOutOfBoundsException.class) + @Test public void testSubstringEndOutOfBounds() { - SourceLine.of("abcd", SourceSpan.of(3, 10, 13, 4)).substring(0, 5); + var sourceLine = SourceLine.of("abcd", SourceSpan.of(3, 10, 13, 4)); + assertThatThrownBy(() -> sourceLine.substring(0, 5)).isInstanceOf(StringIndexOutOfBoundsException.class); } private static void assertSourceLine(SourceLine sourceLine, String expectedContent, SourceSpan expectedSourceSpan) { - assertEquals(expectedContent, sourceLine.getContent()); - assertEquals(expectedSourceSpan, sourceLine.getSourceSpan()); + assertThat(sourceLine.getContent()).isEqualTo(expectedContent); + assertThat(sourceLine.getSourceSpan()).isEqualTo(expectedSourceSpan); } } diff --git a/commonmark/src/test/java/org/commonmark/test/SourceSpanTest.java b/commonmark/src/test/java/org/commonmark/test/SourceSpanTest.java index 57048b90e..f1bb231f4 100644 --- a/commonmark/src/test/java/org/commonmark/test/SourceSpanTest.java +++ b/commonmark/src/test/java/org/commonmark/test/SourceSpanTest.java @@ -1,10 +1,10 @@ package org.commonmark.test; import org.commonmark.node.SourceSpan; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertSame; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class SourceSpanTest { @@ -12,52 +12,57 @@ public class SourceSpanTest { public void testSubSpan() { var span = SourceSpan.of(1, 2, 3, 5); - assertSame(span.subSpan(0), span); - assertSame(span.subSpan(0, 5), span); + assertThat(span.subSpan(0)).isSameAs(span); + assertThat(span.subSpan(0, 5)).isSameAs(span); - assertEquals(SourceSpan.of(1, 3, 4, 4), span.subSpan(1)); - assertEquals(SourceSpan.of(1, 4, 5, 3), span.subSpan(2)); - assertEquals(SourceSpan.of(1, 5, 6, 2), span.subSpan(3)); - assertEquals(SourceSpan.of(1, 6, 7, 1), span.subSpan(4)); + assertThat(span.subSpan(1)).isEqualTo(SourceSpan.of(1, 3, 4, 4)); + assertThat(span.subSpan(2)).isEqualTo(SourceSpan.of(1, 4, 5, 3)); + assertThat(span.subSpan(3)).isEqualTo(SourceSpan.of(1, 5, 6, 2)); + assertThat(span.subSpan(4)).isEqualTo(SourceSpan.of(1, 6, 7, 1)); // Not sure if empty spans are useful, but it probably makes sense to mirror how substrings work - assertEquals(SourceSpan.of(1, 7, 8, 0), span.subSpan(5)); - assertEquals("", "abcde".substring(5)); - - assertEquals(SourceSpan.of(1, 2, 3, 5), span.subSpan(0, 5)); - assertEquals(SourceSpan.of(1, 2, 3, 4), span.subSpan(0, 4)); - assertEquals(SourceSpan.of(1, 2, 3, 3), span.subSpan(0, 3)); - assertEquals(SourceSpan.of(1, 2, 3, 2), span.subSpan(0, 2)); - assertEquals(SourceSpan.of(1, 2, 3, 1), span.subSpan(0, 1)); - assertEquals(SourceSpan.of(1, 2, 3, 0), span.subSpan(0, 0)); - assertEquals("a", "abcde".substring(0, 1)); - assertEquals("", "abcde".substring(0, 0)); - - assertEquals(SourceSpan.of(1, 3, 4, 3), span.subSpan(1, 4)); - assertEquals(SourceSpan.of(1, 4, 5, 1), span.subSpan(2, 3)); + assertThat(span.subSpan(5)).isEqualTo(SourceSpan.of(1, 7, 8, 0)); + assertThat("abcde".substring(5)).isEqualTo(""); + + assertThat(span.subSpan(0, 5)).isEqualTo(SourceSpan.of(1, 2, 3, 5)); + assertThat(span.subSpan(0, 4)).isEqualTo(SourceSpan.of(1, 2, 3, 4)); + assertThat(span.subSpan(0, 3)).isEqualTo(SourceSpan.of(1, 2, 3, 3)); + assertThat(span.subSpan(0, 2)).isEqualTo(SourceSpan.of(1, 2, 3, 2)); + assertThat(span.subSpan(0, 1)).isEqualTo(SourceSpan.of(1, 2, 3, 1)); + assertThat(span.subSpan(0, 0)).isEqualTo(SourceSpan.of(1, 2, 3, 0)); + assertThat("abcde".substring(0, 1)).isEqualTo("a"); + assertThat("abcde".substring(0, 0)).isEqualTo(""); + + assertThat(span.subSpan(1, 4)).isEqualTo(SourceSpan.of(1, 3, 4, 3)); + assertThat(span.subSpan(2, 3)).isEqualTo(SourceSpan.of(1, 4, 5, 1)); } - @Test(expected = IndexOutOfBoundsException.class) + @Test public void testSubSpanBeginIndexNegative() { - SourceSpan.of(1, 2, 3, 5).subSpan(-1); + var sourceSpan = SourceSpan.of(1, 2, 3, 5); + assertThatThrownBy(() -> sourceSpan.subSpan(-1)).isInstanceOf(IndexOutOfBoundsException.class); } - @Test(expected = IndexOutOfBoundsException.class) + @Test public void testSubSpanBeginIndexOutOfBounds() { - SourceSpan.of(1, 2, 3, 5).subSpan(6); + var sourceSpan = SourceSpan.of(1, 2, 3, 5); + assertThatThrownBy(() -> sourceSpan.subSpan(6)).isInstanceOf(IndexOutOfBoundsException.class); } - @Test(expected = IndexOutOfBoundsException.class) + @Test public void testSubSpanEndIndexNegative() { - SourceSpan.of(1, 2, 3, 5).subSpan(0, -1); + var sourceSpan = SourceSpan.of(1, 2, 3, 5); + assertThatThrownBy(() -> sourceSpan.subSpan(0, -1)).isInstanceOf(IndexOutOfBoundsException.class); } - @Test(expected = IndexOutOfBoundsException.class) + @Test public void testSubSpanEndIndexOutOfBounds() { - SourceSpan.of(1, 2, 3, 5).subSpan(0, 6); + var sourceSpan = SourceSpan.of(1, 2, 3, 5); + assertThatThrownBy(() -> sourceSpan.subSpan(0, 6)).isInstanceOf(IndexOutOfBoundsException.class); } - @Test(expected = IndexOutOfBoundsException.class) + @Test public void testSubSpanBeginIndexGreaterThanEndIndex() { - SourceSpan.of(1, 2, 3, 5).subSpan(2, 1); + var sourceSpan = SourceSpan.of(1, 2, 3, 5); + assertThatThrownBy(() -> sourceSpan.subSpan(2, 1)).isInstanceOf(IndexOutOfBoundsException.class); } } diff --git a/commonmark/src/test/java/org/commonmark/test/SourceSpansTest.java b/commonmark/src/test/java/org/commonmark/test/SourceSpansTest.java index 23249d930..f4e9d0a17 100644 --- a/commonmark/src/test/java/org/commonmark/test/SourceSpansTest.java +++ b/commonmark/src/test/java/org/commonmark/test/SourceSpansTest.java @@ -3,7 +3,7 @@ import org.commonmark.node.*; import org.commonmark.parser.IncludeSourceSpans; import org.commonmark.parser.Parser; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.StringReader; @@ -11,7 +11,7 @@ import java.util.Deque; import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class SourceSpansTest { @@ -90,7 +90,7 @@ public void fencedCodeBlock() { Node document = PARSER.parse("```\nfoo\n```\nbar\n"); Paragraph paragraph = (Paragraph) document.getLastChild(); - assertEquals(List.of(SourceSpan.of(3, 0, 12, 3)), paragraph.getSourceSpans()); + assertThat(paragraph.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(3, 0, 12, 3))); } @Test @@ -133,7 +133,7 @@ public void listBlock() { Node document = PARSER.parse("* foo\n * bar\n"); ListBlock listBlock = (ListBlock) document.getFirstChild().getFirstChild().getLastChild(); - assertEquals(List.of(SourceSpan.of(1, 2, 8, 5)), listBlock.getSourceSpans()); + assertThat(listBlock.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(1, 2, 8, 5))); } @Test @@ -158,10 +158,10 @@ public void linkReferenceDefinition() { Node document = PARSER.parse("[foo]: /url\ntext\n"); LinkReferenceDefinition linkReferenceDefinition = (LinkReferenceDefinition) document.getFirstChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 11)), linkReferenceDefinition.getSourceSpans()); + assertThat(linkReferenceDefinition.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 11))); Paragraph paragraph = (Paragraph) document.getLastChild(); - assertEquals(List.of(SourceSpan.of(1, 0, 12, 4)), paragraph.getSourceSpans()); + assertThat(paragraph.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(1, 0, 12, 4))); } @Test @@ -169,8 +169,8 @@ public void linkReferenceDefinitionMultiple() { var doc = PARSER.parse("[foo]: /foo\n[bar]: /bar\n"); var def1 = (LinkReferenceDefinition) doc.getFirstChild(); var def2 = (LinkReferenceDefinition) doc.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 11)), def1.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(1, 0, 12, 11)), def2.getSourceSpans()); + assertThat(def1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 11))); + assertThat(def2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(1, 0, 12, 11))); } @Test @@ -178,8 +178,8 @@ public void linkReferenceDefinitionWithTitle() { var doc = PARSER.parse("[1]: #not-code \"Text\"\n[foo]: /foo\n"); var def1 = (LinkReferenceDefinition) doc.getFirstChild(); var def2 = (LinkReferenceDefinition) doc.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 21)), def1.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(1, 0, 22, 11)), def2.getSourceSpans()); + assertThat(def1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 21))); + assertThat(def2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(1, 0, 22, 11))); } @Test @@ -187,8 +187,8 @@ public void linkReferenceDefinitionWithTitleInvalid() { var doc = PARSER.parse("[foo]: /url\n\"title\" ok\n"); var def = Nodes.find(doc, LinkReferenceDefinition.class); var paragraph = Nodes.find(doc, Paragraph.class); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 11)), def.getSourceSpans()); - assertEquals(List.of(SourceSpan.of(1, 0, 12, 10)), paragraph.getSourceSpans()); + assertThat(def.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 11))); + assertThat(paragraph.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(1, 0, 12, 10))); } @Test @@ -198,10 +198,10 @@ public void linkReferenceDefinitionHeading() { Node document = PARSER.parse("[foo]: /url\nHeading\n===\n"); LinkReferenceDefinition linkReferenceDefinition = (LinkReferenceDefinition) document.getFirstChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 11)), linkReferenceDefinition.getSourceSpans()); + assertThat(linkReferenceDefinition.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 11))); Heading heading = (Heading) document.getLastChild(); - assertEquals(List.of(SourceSpan.of(1, 0, 12, 7), SourceSpan.of(2, 0, 20, 3)), heading.getSourceSpans()); + assertThat(heading.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(1, 0, 12, 7), SourceSpan.of(2, 0, 20, 3))); } @Test @@ -212,13 +212,13 @@ public void lazyContinuationLines() { var doc = PARSER.parse("> > > foo\nbar\n"); var bq1 = (BlockQuote) doc.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 9), SourceSpan.of(1, 0, 10, 3)), bq1.getSourceSpans()); + assertThat(bq1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 9), SourceSpan.of(1, 0, 10, 3))); var bq2 = (BlockQuote) bq1.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 2, 2, 7), SourceSpan.of(1, 0, 10, 3)), bq2.getSourceSpans()); + assertThat(bq2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 2, 2, 7), SourceSpan.of(1, 0, 10, 3))); var bq3 = (BlockQuote) bq2.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 4, 4, 5), SourceSpan.of(1, 0, 10, 3)), bq3.getSourceSpans()); + assertThat(bq3.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 4, 4, 5), SourceSpan.of(1, 0, 10, 3))); var paragraph = (Paragraph) bq3.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 6, 6, 3), SourceSpan.of(1, 0, 10, 3)), paragraph.getSourceSpans()); + assertThat(paragraph.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 6, 6, 3), SourceSpan.of(1, 0, 10, 3))); } { @@ -226,13 +226,13 @@ public void lazyContinuationLines() { var doc = PARSER.parse("> > > foo\nbars\n"); var bq1 = (BlockQuote) doc.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 9), SourceSpan.of(1, 0, 10, 4)), bq1.getSourceSpans()); + assertThat(bq1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 9), SourceSpan.of(1, 0, 10, 4))); var bq2 = (BlockQuote) bq1.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 2, 2, 7), SourceSpan.of(1, 0, 10, 4)), bq2.getSourceSpans()); + assertThat(bq2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 2, 2, 7), SourceSpan.of(1, 0, 10, 4))); var bq3 = (BlockQuote) bq2.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 4, 4, 5), SourceSpan.of(1, 0, 10, 4)), bq3.getSourceSpans()); + assertThat(bq3.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 4, 4, 5), SourceSpan.of(1, 0, 10, 4))); var paragraph = (Paragraph) bq3.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 6, 6, 3), SourceSpan.of(1, 0, 10, 4)), paragraph.getSourceSpans()); + assertThat(paragraph.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 6, 6, 3), SourceSpan.of(1, 0, 10, 4))); } { @@ -240,15 +240,15 @@ public void lazyContinuationLines() { var doc = PARSER.parse("> 1. > Blockquote\ncontinued here."); var bq1 = (BlockQuote) doc.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 17), SourceSpan.of(1, 0, 18, 15)), bq1.getSourceSpans()); + assertThat(bq1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 17), SourceSpan.of(1, 0, 18, 15))); var orderedList = (OrderedList) bq1.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 2, 2, 15), SourceSpan.of(1, 0, 18, 15)), orderedList.getSourceSpans()); + assertThat(orderedList.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 2, 2, 15), SourceSpan.of(1, 0, 18, 15))); var listItem = (ListItem) orderedList.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 2, 2, 15), SourceSpan.of(1, 0, 18, 15)), listItem.getSourceSpans()); + assertThat(listItem.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 2, 2, 15), SourceSpan.of(1, 0, 18, 15))); var bq2 = (BlockQuote) listItem.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 5, 5, 12), SourceSpan.of(1, 0, 18, 15)), bq2.getSourceSpans()); + assertThat(bq2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 5, 5, 12), SourceSpan.of(1, 0, 18, 15))); var paragraph = (Paragraph) bq2.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 7, 7, 10), SourceSpan.of(1, 0, 18, 15)), paragraph.getSourceSpans()); + assertThat(paragraph.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 7, 7, 10), SourceSpan.of(1, 0, 18, 15))); } { @@ -256,11 +256,11 @@ public void lazyContinuationLines() { var doc = PARSER.parse("> > foo\n> bar\n"); var bq1 = (BlockQuote) doc.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 0, 0, 7), SourceSpan.of(1, 0, 8, 5)), bq1.getSourceSpans()); + assertThat(bq1.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 0, 0, 7), SourceSpan.of(1, 0, 8, 5))); var bq2 = (BlockQuote) bq1.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 2, 2, 5), SourceSpan.of(1, 2, 10, 3)), bq2.getSourceSpans()); + assertThat(bq2.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 2, 2, 5), SourceSpan.of(1, 2, 10, 3))); var paragraph = (Paragraph) bq2.getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 4, 4, 3), SourceSpan.of(1, 2, 10, 3)), paragraph.getSourceSpans()); + assertThat(paragraph.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 4, 4, 3), SourceSpan.of(1, 2, 10, 3))); } } @@ -354,7 +354,7 @@ public void inlineEmphasis() { Node document = INLINES_PARSER.parse("*hey**"); Node lastText = document.getFirstChild().getLastChild(); - assertEquals(List.of(SourceSpan.of(0, 5, 5, 1)), lastText.getSourceSpans()); + assertThat(lastText.getSourceSpans()).isEqualTo(List.of(SourceSpan.of(0, 5, 5, 1))); } @Test @@ -381,8 +381,8 @@ public void differentLineTerminators() { private void assertVisualize(String source, String expected) { var doc = PARSER.parse(source); - assertEquals(expected, SourceSpanRenderer.renderWithLineColumn(doc, source)); - assertEquals(expected, SourceSpanRenderer.renderWithInputIndex(doc, source)); + assertThat(SourceSpanRenderer.renderWithLineColumn(doc, source)).isEqualTo(expected); + assertThat(SourceSpanRenderer.renderWithInputIndex(doc, source)).isEqualTo(expected); } private static void assertSpans(String input, Class nodeClass, SourceSpan... expectedSourceSpans) { @@ -405,7 +405,7 @@ private static void assertInlineSpans(String input, Class nodeCl private static void assertSpans(Node rootNode, Class nodeClass, SourceSpan... expectedSourceSpans) { Node node = findNode(rootNode, nodeClass); - assertEquals(List.of(expectedSourceSpans), node.getSourceSpans()); + assertThat(node.getSourceSpans()).isEqualTo(List.of(expectedSourceSpans)); } private static Node findNode(Node rootNode, Class nodeClass) { diff --git a/commonmark/src/test/java/org/commonmark/test/SpecCoreTest.java b/commonmark/src/test/java/org/commonmark/test/SpecCoreTest.java index 8d284c6f9..fefd8fb30 100644 --- a/commonmark/src/test/java/org/commonmark/test/SpecCoreTest.java +++ b/commonmark/src/test/java/org/commonmark/test/SpecCoreTest.java @@ -7,10 +7,10 @@ import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.SpecTestCase; import org.commonmark.testutil.example.Example; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.fail; import static org.commonmark.testutil.Asserts.assertRendering; -import static org.junit.Assert.fail; public class SpecCoreTest extends SpecTestCase { @@ -18,10 +18,6 @@ public class SpecCoreTest extends SpecTestCase { // The spec says URL-escaping is optional, but the examples assume that it's enabled. private static final HtmlRenderer RENDERER = HtmlRenderer.builder().percentEncodeUrls(true).build(); - public SpecCoreTest(Example example) { - super(example); - } - @Test public void testTextNodesContiguous() { final String source = example.getSource(); diff --git a/commonmark/src/test/java/org/commonmark/test/SpecCrLfCoreTest.java b/commonmark/src/test/java/org/commonmark/test/SpecCrLfCoreTest.java index ab0103e5a..47ca3da4e 100644 --- a/commonmark/src/test/java/org/commonmark/test/SpecCrLfCoreTest.java +++ b/commonmark/src/test/java/org/commonmark/test/SpecCrLfCoreTest.java @@ -4,7 +4,7 @@ import org.commonmark.renderer.html.HtmlRenderer; import org.commonmark.testutil.SpecTestCase; import org.commonmark.testutil.example.Example; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.commonmark.testutil.Asserts.assertRendering; @@ -17,10 +17,6 @@ public class SpecCrLfCoreTest extends SpecTestCase { // The spec says URL-escaping is optional, but the examples assume that it's enabled. private static final HtmlRenderer RENDERER = HtmlRenderer.builder().percentEncodeUrls(true).build(); - public SpecCrLfCoreTest(Example example) { - super(example); - } - @Test public void testHtmlRendering() { assertRendering(example.getSource(), example.getHtml(), render(example.getSource())); diff --git a/commonmark/src/test/java/org/commonmark/test/SpecialInputTest.java b/commonmark/src/test/java/org/commonmark/test/SpecialInputTest.java index 770a1aa21..45cd3aea2 100644 --- a/commonmark/src/test/java/org/commonmark/test/SpecialInputTest.java +++ b/commonmark/src/test/java/org/commonmark/test/SpecialInputTest.java @@ -1,6 +1,6 @@ package org.commonmark.test; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class SpecialInputTest extends CoreRenderingTestCase { @@ -210,4 +210,19 @@ public void htmlBlockInterruptingList() { "\n" + "\n"); } + + @Test + public void emphasisAfterHardLineBreak() { + assertRendering("Hello \n" + + "**Bar**\n" + + "Foo\n", "

Hello
\n" + + "Bar\n" + + "Foo

\n"); + + assertRendering("Hello \n" + + "**Bar** \n" + + "Foo\n", "

Hello
\n" + + "Bar
\n" + + "Foo

\n"); + } } diff --git a/commonmark/src/test/java/org/commonmark/test/TextContentRendererTest.java b/commonmark/src/test/java/org/commonmark/test/TextContentRendererTest.java index 93ca87d93..46757e0c3 100644 --- a/commonmark/src/test/java/org/commonmark/test/TextContentRendererTest.java +++ b/commonmark/src/test/java/org/commonmark/test/TextContentRendererTest.java @@ -9,7 +9,7 @@ import org.commonmark.renderer.text.TextContentRenderer; import org.commonmark.parser.Parser; import org.commonmark.testutil.Asserts; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.util.Set; @@ -116,14 +116,14 @@ public void textContentLists() { assertSeparate(s, "bar\n\n1. foo\n 1. bar\n2. foo"); assertStripped(s, "bar 1. foo 1. bar 2. foo"); - s = "bar\n* foo\n - bar\n* foo"; - assertCompact(s, "bar\n* foo\n - bar\n* foo"); - assertSeparate(s, "bar\n\n* foo\n - bar\n* foo"); + s = "bar\n* foo\n - bar\n* foo"; + assertCompact(s, "bar\n* foo\n - bar\n* foo"); + assertSeparate(s, "bar\n\n* foo\n - bar\n* foo"); assertStripped(s, "bar foo bar foo"); - s = "bar\n* foo\n 1. bar\n 2. bar\n* foo"; - assertCompact(s, "bar\n* foo\n 1. bar\n 2. bar\n* foo"); - assertSeparate(s, "bar\n\n* foo\n 1. bar\n 2. bar\n* foo"); + s = "bar\n* foo\n 1. bar\n 2. bar\n* foo"; + assertCompact(s, "bar\n* foo\n 1. bar\n 2. bar\n* foo"); + assertSeparate(s, "bar\n\n* foo\n 1. bar\n 2. bar\n* foo"); assertStripped(s, "bar foo 1. bar 2. bar foo"); s = "bar\n1. foo\n * bar\n * bar\n2. foo"; @@ -196,6 +196,17 @@ public void textContentHtml() { assertAll(html, html); } + @Test + public void testContentNestedLists() { + var s = "List:\n" + + "1. 2) 3. \n" + + "end"; + assertCompact(s, s); + + var s2 = "1. A\n 1) B\n 1. Test"; + assertCompact(s2, s2); + } + @Test public void testOverrideNodeRendering() { var nodeRendererFactory = new TextContentNodeRendererFactory() { diff --git a/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java b/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java index 0be668a70..a9f37792e 100644 --- a/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java +++ b/commonmark/src/test/java/org/commonmark/test/TextContentWriterTest.java @@ -1,9 +1,9 @@ package org.commonmark.test; import org.commonmark.renderer.text.TextContentWriter; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.*; +import static org.assertj.core.api.Assertions.assertThat; public class TextContentWriterTest { @@ -14,7 +14,7 @@ public void whitespace() throws Exception { writer.write("foo"); writer.whitespace(); writer.write("bar"); - assertEquals("foo bar", stringBuilder.toString()); + assertThat(stringBuilder.toString()).isEqualTo("foo bar"); } @Test @@ -24,7 +24,7 @@ public void colon() throws Exception { writer.write("foo"); writer.colon(); writer.write("bar"); - assertEquals("foo:bar", stringBuilder.toString()); + assertThat(stringBuilder.toString()).isEqualTo("foo:bar"); } @Test @@ -34,7 +34,7 @@ public void line() throws Exception { writer.write("foo"); writer.line(); writer.write("bar"); - assertEquals("foo\nbar", stringBuilder.toString()); + assertThat(stringBuilder.toString()).isEqualTo("foo\nbar"); } @Test @@ -42,7 +42,7 @@ public void writeStripped() throws Exception { StringBuilder stringBuilder = new StringBuilder(); TextContentWriter writer = new TextContentWriter(stringBuilder); writer.writeStripped("foo\n bar"); - assertEquals("foo bar", stringBuilder.toString()); + assertThat(stringBuilder.toString()).isEqualTo("foo bar"); } @Test @@ -50,6 +50,6 @@ public void write() throws Exception { StringBuilder stringBuilder = new StringBuilder(); TextContentWriter writer = new TextContentWriter(stringBuilder); writer.writeStripped("foo bar"); - assertEquals("foo bar", stringBuilder.toString()); + assertThat(stringBuilder.toString()).isEqualTo("foo bar"); } } diff --git a/commonmark/src/test/java/org/commonmark/test/ThematicBreakParserTest.java b/commonmark/src/test/java/org/commonmark/test/ThematicBreakParserTest.java index 2b15f8add..1d564cca2 100644 --- a/commonmark/src/test/java/org/commonmark/test/ThematicBreakParserTest.java +++ b/commonmark/src/test/java/org/commonmark/test/ThematicBreakParserTest.java @@ -2,9 +2,9 @@ import org.commonmark.node.ThematicBreak; import org.commonmark.parser.Parser; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class ThematicBreakParserTest { @@ -20,6 +20,6 @@ public void testLiteral() { private static void assertLiteral(String expected, String input) { var tb = Nodes.find(PARSER.parse(input), ThematicBreak.class); - assertEquals(expected, tb.getLiteral()); + assertThat(tb.getLiteral()).isEqualTo(expected); } } diff --git a/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java b/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java index 63901a49b..20cd9f5ab 100644 --- a/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java +++ b/commonmark/src/test/java/org/commonmark/test/UsageExampleTest.java @@ -6,8 +6,8 @@ import org.commonmark.renderer.NodeRenderer; import org.commonmark.renderer.html.*; import org.commonmark.renderer.markdown.MarkdownRenderer; -import org.junit.Ignore; -import org.junit.Test; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import java.io.FileInputStream; import java.io.IOException; @@ -16,7 +16,7 @@ import java.util.Map; import java.util.Set; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; public class UsageExampleTest { @@ -25,7 +25,7 @@ public void parseAndRender() { Parser parser = Parser.builder().build(); Node document = parser.parse("This is *Markdown*"); HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build(); - assertEquals("

This is Markdown

\n", renderer.render(document)); + assertThat(renderer.render(document)).isEqualTo("

This is Markdown

\n"); } @Test @@ -37,11 +37,11 @@ public void renderToMarkdown() { heading.appendChild(new Text("My title")); document.appendChild(heading); - assertEquals("## My title\n", renderer.render(document)); + assertThat(renderer.render(document)).isEqualTo("## My title\n"); } @Test - @Ignore + @Disabled public void parseReaderRender() throws IOException { Parser parser = Parser.builder().build(); try (InputStreamReader reader = new InputStreamReader(new FileInputStream("file.md"), StandardCharsets.UTF_8)) { @@ -56,7 +56,7 @@ public void visitor() { Node node = parser.parse("Example\n=======\n\nSome more text"); WordCountVisitor visitor = new WordCountVisitor(); node.accept(visitor); - assertEquals(4, visitor.wordCount); + assertThat(visitor.wordCount).isEqualTo(4); } @Test @@ -67,11 +67,11 @@ public void sourcePositions() { var doc = parser.parse(source); var emphasis = doc.getLastChild().getLastChild(); var s = emphasis.getSourceSpans().get(0); - assertEquals(2, s.getLineIndex()); - assertEquals(4, s.getColumnIndex()); - assertEquals(9, s.getInputIndex()); - assertEquals(5, s.getLength()); - assertEquals("*baz*", source.substring(s.getInputIndex(), s.getInputIndex() + s.getLength())); + assertThat(s.getLineIndex()).isEqualTo(2); + assertThat(s.getColumnIndex()).isEqualTo(4); + assertThat(s.getInputIndex()).isEqualTo(9); + assertThat(s.getLength()).isEqualTo(5); + assertThat(source.substring(s.getInputIndex(), s.getInputIndex() + s.getLength())).isEqualTo("*baz*"); } @Test @@ -87,8 +87,7 @@ public AttributeProvider create(AttributeProviderContext context) { .build(); Node document = parser.parse("![text](/url.png)"); - assertEquals("

\"text\"

\n", - renderer.render(document)); + assertThat(renderer.render(document)).isEqualTo("

\"text\"

\n"); } @Test @@ -104,7 +103,7 @@ public NodeRenderer create(HtmlNodeRendererContext context) { .build(); Node document = parser.parse("Example:\n\n code"); - assertEquals("

Example:

\n
code\n
\n", renderer.render(document)); + assertThat(renderer.render(document)).isEqualTo("

Example:

\n
code\n
\n"); } class WordCountVisitor extends AbstractVisitor { diff --git a/commonmark/src/test/java/org/commonmark/text/CharactersTest.java b/commonmark/src/test/java/org/commonmark/text/CharactersTest.java index a362cf53c..99f510cb7 100644 --- a/commonmark/src/test/java/org/commonmark/text/CharactersTest.java +++ b/commonmark/src/test/java/org/commonmark/text/CharactersTest.java @@ -1,9 +1,8 @@ package org.commonmark.text; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; public class CharactersTest { @@ -18,17 +17,17 @@ public void isPunctuation() { }; for (char c : chars) { - assertTrue("Expected to be punctuation: " + c, Characters.isPunctuationCodePoint(c)); + assertThat(Characters.isPunctuationCodePoint(c)).as("Expected to be punctuation: " + c).isTrue(); } } @Test public void isBlank() { - assertTrue(Characters.isBlank("")); - assertTrue(Characters.isBlank(" ")); - assertTrue(Characters.isBlank("\t")); - assertTrue(Characters.isBlank(" \t")); - assertFalse(Characters.isBlank("a")); - assertFalse(Characters.isBlank("\f")); + assertThat(Characters.isBlank("")).isTrue(); + assertThat(Characters.isBlank(" ")).isTrue(); + assertThat(Characters.isBlank("\t")).isTrue(); + assertThat(Characters.isBlank(" \t")).isTrue(); + assertThat(Characters.isBlank("a")).isFalse(); + assertThat(Characters.isBlank("\f")).isFalse(); } } diff --git a/pom.xml b/pom.xml index 031869f6e..f12805316 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.commonmark commonmark-parent - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT commonmark-java parent Java implementation of CommonMark, a specification of the Markdown format for turning plain text into formatted @@ -17,6 +17,7 @@ commonmark commonmark-ext-autolink commonmark-ext-footnotes + commonmark-ext-gfm-alerts commonmark-ext-gfm-strikethrough commonmark-ext-gfm-tables commonmark-ext-heading-anchor @@ -39,7 +40,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.14.0 11 @@ -47,17 +48,22 @@ org.apache.maven.plugins maven-jar-plugin - 3.4.1 + 3.4.2 + + + ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + org.apache.maven.plugins maven-install-plugin - 3.1.1 + 3.1.4 org.apache.maven.plugins maven-javadoc-plugin - 3.6.3 + 3.11.2 *.internal,*.internal.* @@ -74,28 +80,28 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.5 + 3.5.3 + - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - ossrh - https://oss.sonatype.org/ - true - 10 + central + true + published org.apache.maven.plugins maven-release-plugin - 3.0.1 + 3.1.1 true false @@ -103,6 +109,21 @@ deploy + + org.apache.felix + maven-bundle-plugin + + 5.1.9 + + + bundle-manifest + process-classes + + manifest + + + + @@ -112,59 +133,74 @@ org.commonmark commonmark - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT org.commonmark commonmark-ext-autolink - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT + + + org.commonmark + commonmark-ext-footnotes + 0.28.1-SNAPSHOT org.commonmark commonmark-ext-image-attributes - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT org.commonmark commonmark-ext-ins - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT + + + org.commonmark + commonmark-ext-gfm-alerts + 0.28.1-SNAPSHOT org.commonmark commonmark-ext-gfm-strikethrough - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT org.commonmark commonmark-ext-gfm-tables - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT org.commonmark commonmark-ext-heading-anchor - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT org.commonmark commonmark-ext-task-list-items - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT org.commonmark commonmark-ext-yaml-front-matter - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT org.commonmark commonmark-test-util - 0.24.1-SNAPSHOT + 0.28.1-SNAPSHOT - junit - junit - 4.13.1 + org.junit.jupiter + junit-jupiter + 5.13.1 + + + org.assertj + assertj-core + 3.27.7 org.openjdk.jmh @@ -212,7 +248,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.2.4 + 3.2.7 sign-artifacts @@ -239,7 +275,7 @@ org.jacoco jacoco-maven-plugin - 0.8.12 + 0.8.13 @@ -282,15 +318,4 @@ HEAD - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - -