Skip to content

Commit

Permalink
Merge pull request #272 from commonmark/issue-271-strikethrough-two-t…
Browse files Browse the repository at this point in the history
…ildes

Add `requireTwoTildes` for `StrikethroughExtension` (fixes #271)
  • Loading branch information
robinst committed Oct 27, 2022
2 parents ba673a9 + de1c59e commit 376fc8e
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 11 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Expand Up @@ -6,6 +6,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html),
with the exception that 0.x versions can break between minor versions.

## [Unreleased]
### Added
- GitHub strikethrough: With the previous version we adjusted the
extension to also accept the single tilde syntax. But if you use
another extension that uses the single tilde syntax, you will get a
conflict. To avoid that, `StrikethroughExtension` can now be
configured to require two tildes like before, see Javadoc.

## [0.20.0] - 2022-10-20
### Fixed
- GitHub tables: A single pipe (optional whitespace) now ends a table
Expand Down Expand Up @@ -371,7 +379,7 @@ 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.20.0...HEAD
[0.20.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.19.0...commonmark-parent-0.20.0
[0.19.0]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.18.2...commonmark-parent-0.19.0
[0.18.2]: https://github.com/commonmark/commonmark-java/compare/commonmark-parent-0.18.1...commonmark-parent-0.18.2
Expand Down
Expand Up @@ -14,29 +14,59 @@
import org.commonmark.renderer.NodeRenderer;

/**
* Extension for GFM strikethrough using ~~ (GitHub Flavored Markdown).
* Extension for GFM strikethrough using {@code ~} or {@code ~~} (GitHub Flavored Markdown).
* <p>Example input:</p>
* <pre>{@code ~foo~ or ~~bar~~}</pre>
* <p>Example output (HTML):</p>
* <pre>{@code <del>foo</del> or <del>bar</del>}</pre>
* <p>
* Create it with {@link #create()} and then configure it on the builders
* Create the extension with {@link #create()} and then add it to the parser and renderer builders
* ({@link org.commonmark.parser.Parser.Builder#extensions(Iterable)},
* {@link HtmlRenderer.Builder#extensions(Iterable)}).
* </p>
* <p>
* The parsed strikethrough text regions are turned into {@link Strikethrough} nodes.
* </p>
* <p>
* If you have another extension that only uses a single tilde ({@code ~}) syntax, you will have to configure this
* {@link StrikethroughExtension} to only accept the double tilde syntax, like this:
* </p>
* <pre>
* {@code
* StrikethroughExtension.builder().requireTwoTildes(true).build();
* }
* </pre>
* <p>
* If you don't do that, there's a conflict between the two extensions and you will get an
* {@link IllegalArgumentException} when constructing the parser.
* </p>
*/
public class StrikethroughExtension implements Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension,
TextContentRenderer.TextContentRendererExtension {

private StrikethroughExtension() {
private final boolean requireTwoTildes;

private StrikethroughExtension(Builder builder) {
this.requireTwoTildes = builder.requireTwoTildes;
}

/**
* @return the extension with default options
*/
public static Extension create() {
return new StrikethroughExtension();
return builder().build();
}

/**
* @return a builder to configure the behavior of the extension
*/
public static Builder builder() {
return new Builder();
}

@Override
public void extend(Parser.Builder parserBuilder) {
parserBuilder.customDelimiterProcessor(new StrikethroughDelimiterProcessor());
parserBuilder.customDelimiterProcessor(new StrikethroughDelimiterProcessor(requireTwoTildes));
}

@Override
Expand All @@ -58,4 +88,26 @@ public NodeRenderer create(TextContentNodeRendererContext context) {
}
});
}

public static class Builder {

private boolean requireTwoTildes = false;

/**
* @param requireTwoTildes Whether two tilde characters ({@code ~~}) are required for strikethrough or whether
* one is also enough. Default is {@code false}; both a single tilde and two tildes can be used for strikethrough.
* @return {@code this}
*/
public Builder requireTwoTildes(boolean requireTwoTildes) {
this.requireTwoTildes = requireTwoTildes;
return this;
}

/**
* @return a configured extension
*/
public Extension build() {
return new StrikethroughExtension(this);
}
}
}
Expand Up @@ -10,6 +10,16 @@

public class StrikethroughDelimiterProcessor implements DelimiterProcessor {

private final boolean requireTwoTildes;

public StrikethroughDelimiterProcessor() {
this(false);
}

public StrikethroughDelimiterProcessor(boolean requireTwoTildes) {
this.requireTwoTildes = requireTwoTildes;
}

@Override
public char getOpeningCharacter() {
return '~';
Expand All @@ -22,7 +32,7 @@ public char getClosingCharacter() {

@Override
public int getMinLength() {
return 1;
return requireTwoTildes ? 2 : 1;
}

@Override
Expand Down
Expand Up @@ -4,22 +4,25 @@
import org.commonmark.node.Node;
import org.commonmark.node.Paragraph;
import org.commonmark.node.SourceSpan;
import org.commonmark.node.Text;
import org.commonmark.parser.IncludeSourceSpans;
import org.commonmark.parser.Parser;
import org.commonmark.parser.delimiter.DelimiterProcessor;
import org.commonmark.parser.delimiter.DelimiterRun;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.text.TextContentRenderer;
import org.commonmark.testutil.RenderingTestCase;
import org.junit.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.Set;

import static java.util.Collections.singleton;
import static org.junit.Assert.assertEquals;

public class StrikethroughTest extends RenderingTestCase {

private static final Set<Extension> EXTENSIONS = Collections.singleton(StrikethroughExtension.create());
private static final Set<Extension> EXTENSIONS = singleton(StrikethroughExtension.create());
private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS).build();
private static final HtmlRenderer HTML_RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS).build();
private static final TextContentRenderer CONTENT_RENDERER = TextContentRenderer.builder()
Expand Down Expand Up @@ -92,6 +95,19 @@ public void textContentRenderer() {
assertEquals("/foo/", CONTENT_RENDERER.render(document));
}

@Test
public void requireTwoTildesOption() {
Parser parser = Parser.builder()
.extensions(singleton(StrikethroughExtension.builder()
.requireTwoTildes(true)
.build()))
.customDelimiterProcessor(new SubscriptDelimiterProcessor())
.build();

Node document = parser.parse("~foo~ ~~bar~~");
assertEquals("(sub)foo(/sub) /bar/", CONTENT_RENDERER.render(document));
}

@Test
public void sourceSpans() {
Parser parser = Parser.builder()
Expand All @@ -110,4 +126,29 @@ public void sourceSpans() {
protected String render(String source) {
return HTML_RENDERER.render(PARSER.parse(source));
}

private static class SubscriptDelimiterProcessor implements DelimiterProcessor {

@Override
public char getOpeningCharacter() {
return '~';
}

@Override
public char getClosingCharacter() {
return '~';
}

@Override
public int getMinLength() {
return 1;
}

@Override
public int process(DelimiterRun openingRun, DelimiterRun closingRun) {
openingRun.getOpener().insertAfter(new Text("(sub)"));
closingRun.getCloser().insertBefore(new Text("(/sub)"));
return 1;
}
}
}
Expand Up @@ -51,7 +51,7 @@ void add(DelimiterProcessor dp) {
added = true;
break;
} else if (len == pLen) {
throw new IllegalArgumentException("Cannot add two delimiter processors for char '" + delim + "' and minimum length " + len);
throw new IllegalArgumentException("Cannot add two delimiter processors for char '" + delim + "' and minimum length " + len + "; conflicting processors: " + p + ", " + dp);
}
}
if (!added) {
Expand Down
Expand Up @@ -60,7 +60,7 @@ public void multipleDelimitersWithDifferentLengths() {
}

@Test(expected = IllegalArgumentException.class)
public void multipleDelimitersWithSameLength() {
public void multipleDelimitersWithSameLengthConflict() {
Parser.builder()
.customDelimiterProcessor(new OneDelimiterProcessor())
.customDelimiterProcessor(new OneDelimiterProcessor())
Expand Down

0 comments on commit 376fc8e

Please sign in to comment.