| Author | DaveJarvis <email> |
|---|---|
| Date | 2023-11-20 23:13:29 GMT-0800 |
| Commit | c334bfecbc671ec3443ad04b318e9b8c4f315617 |
| Parent | b7c3eb9 |
| Delta | 291 lines added, 158 lines removed, 133-line increase |
| import com.vladsch.flexmark.html.HtmlRenderer; | ||
| import com.vladsch.flexmark.parser.Parser; | ||
| -import org.junit.jupiter.api.BeforeEach; | ||
| -import org.junit.jupiter.api.Test; | ||
| +import org.junit.jupiter.params.ParameterizedTest; | ||
| +import org.junit.jupiter.params.provider.Arguments; | ||
| +import org.junit.jupiter.params.provider.MethodSource; | ||
| import java.util.List; | ||
| +import java.util.stream.Stream; | ||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| +@SuppressWarnings( "SpellCheckingInspection" ) | ||
| public class CrossReferencesExtensionTest { | ||
| - private Parser mParser; | ||
| - private HtmlRenderer mRenderer; | ||
| - | ||
| - @BeforeEach | ||
| - public void setup() { | ||
| + @ParameterizedTest | ||
| + @MethodSource( "testDocuments" ) | ||
| + public void test_References_Documents_Html( | ||
| + final String input, | ||
| + final String expected ) { | ||
| final var extension = new CrossReferenceExtension(); | ||
| final var pBuilder = Parser.builder(); | ||
| final var hBuilder = HtmlRenderer.builder(); | ||
| final var extensions = List.of( extension ); | ||
| pBuilder.extensions( extensions ); | ||
| hBuilder.extensions( extensions ); | ||
| - | ||
| - mParser = pBuilder.build(); | ||
| - mRenderer = hBuilder.build(); | ||
| - } | ||
| - @Test | ||
| - public void test_References_SingularReferences_ReferencesAndPointers() { | ||
| - final var document = mParser.parse( | ||
| - """ | ||
| - {#fig:cats} [@fig:cats] | ||
| - {#table:dogs} [@table:dogs] | ||
| - {#life:dolphins} [@life:dolphins] | ||
| - """ | ||
| - ); | ||
| + final var parser = pBuilder.build(); | ||
| + final var renderer = hBuilder.build(); | ||
| - final String expected = | ||
| - """ | ||
| - <p><a data-type="fig" name="cats" /> <a data-type="fig" href="#cats" /> | ||
| - <a data-type="table" name="dogs" /> <a data-type="table" href="#dogs" /> | ||
| - <a data-type="life" name="dolphins" /> <a data-type="life" href="#dolphins" /></p> | ||
| - """; | ||
| - final var actual = mRenderer.render( document ); | ||
| + final var document = parser.parse( input ); | ||
| + final var actual = renderer.render( document ); | ||
| assertEquals( expected, actual ); | ||
| } | ||
| - @Test | ||
| - @SuppressWarnings( "SpellCheckingInspection" ) | ||
| - public void test_References_DocumentReferences_ReferencesAndPointers() { | ||
| - final var document = mParser.parse( | ||
| - """ | ||
| - Lorem ipsum dolor sit amet, consectetur adipiscing elit. | ||
| - {#fig:cats} Sed do eiusmod tempor incididunt ut | ||
| - labore et dolore magna aliqua. Ut enim ad minim veniam, | ||
| - quis nostrud exercitation ullamco laboris nisi ut aliquip | ||
| - ex ea commodo consequat. [@fig:cats] | ||
| + private static Stream<Arguments> testDocuments() { | ||
| + return Stream.of( | ||
| + Arguments.of( | ||
| + """ | ||
| + {#fig:cats} [@fig:cats] | ||
| + {#table:dogs} [@table:dogs] | ||
| + {#life:dolphins} [@life:dolphins] | ||
| + """, | ||
| + """ | ||
| + <p><a data-type="fig" name="cats" /> <a data-type="fig" href="#cats" /> | ||
| + <a data-type="table" name="dogs" /> <a data-type="table" href="#dogs" /> | ||
| + <a data-type="life" name="dolphins" /> <a data-type="life" href="#dolphins" /></p> | ||
| + """ ), | ||
| + Arguments.of( | ||
| + """ | ||
| + Lorem ipsum dolor sit amet, consectetur adipiscing elit. | ||
| + {#fig:cats} Sed do eiusmod tempor incididunt ut | ||
| + labore et dolore magna aliqua. Ut enim ad minim veniam, | ||
| + quis nostrud exercitation ullamco laboris nisi ut aliquip | ||
| + ex ea commodo consequat. [@fig:cats] | ||
| + """, | ||
| + """ | ||
| + <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. | ||
| + <a data-type="fig" name="cats" /> Sed do eiusmod tempor incididunt ut | ||
| + labore et dolore magna aliqua. Ut enim ad minim veniam, | ||
| + quis nostrud exercitation ullamco laboris nisi ut aliquip | ||
| + ex ea commodo consequat. <a data-type="fig" href="#cats" /></p> | ||
| + """ | ||
| + ), | ||
| + Arguments.of( | ||
| + """ | ||
| + {#日本:w0mbatß} | ||
| + [@日本:w0mbatß] | ||
| + [@日本:w0mbatß] | ||
| + [@日本:w0mbatß] | ||
| + """, | ||
| + """ | ||
| + <p><a data-type="日本" name="w0mbatß" /> | ||
| + <a data-type="日本" href="#w0mbatß" /> | ||
| + <a data-type="日本" href="#w0mbatß" /> | ||
| + <a data-type="日本" href="#w0mbatß" /></p> | ||
| + """ | ||
| + ), | ||
| + Arguments.of( | ||
| + """ | ||
| + {#note:advancement} | ||
| + | ||
| + Advancement isn't measured by the ingenuity of inventions, but | ||
| + by humanity's ability to anticipate and forfend dire aftermaths | ||
| + *before* using them. | ||
| + | ||
| + [@note:advancement] | ||
| + | ||
| + To what end? | ||
| + """, | ||
| """ | ||
| + <p><a data-type="note" name="advancement" /></p> | ||
| + <p>Advancement isn't measured by the ingenuity of inventions, but | ||
| + by humanity's ability to anticipate and forfend dire aftermaths | ||
| + <em>before</em> using them.</p> | ||
| + <p><a data-type="note" href="#advancement" /></p> | ||
| + <p>To what end?</p> | ||
| + """ | ||
| + ) | ||
| ); | ||
| - | ||
| - final var expected = | ||
| - """ | ||
| - <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. | ||
| - <a data-type="fig" name="cats" /> Sed do eiusmod tempor incididunt ut | ||
| - labore et dolore magna aliqua. Ut enim ad minim veniam, | ||
| - quis nostrud exercitation ullamco laboris nisi ut aliquip | ||
| - ex ea commodo consequat. <a data-type="fig" href="#cats" /></p> | ||
| - """; | ||
| - | ||
| - final var actual = mRenderer.render( document ); | ||
| - | ||
| - assertEquals( expected, actual ); | ||
| } | ||
| } |
| import com.vladsch.flexmark.ast.DelimitedNodeImpl; | ||
| -import com.vladsch.flexmark.parser.core.delimiter.Delimiter; | ||
| import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| import org.jetbrains.annotations.NotNull; | ||
| - | ||
| -import java.util.regex.Pattern; | ||
| - | ||
| -import static java.util.regex.Pattern.compile; | ||
| /** | ||
| * Responsible for writing HTML anchor names in the form | ||
| * {@code <a data-type="..." name="name" />}, where {@code name} can be | ||
| * referred to by a cross-reference. | ||
| * | ||
| * @see AnchorXrefNode | ||
| */ | ||
| class AnchorNameNode extends DelimitedNodeImpl implements CrossReferenceNode { | ||
| - private static final String REGEX_ANCHOR = "#(\\w+):(\\w+)"; | ||
| - private static final Pattern PATTERN_ANCHOR = compile( REGEX_ANCHOR ); | ||
| private BasedSequence mOpeningMarker = BasedSequence.EMPTY; | ||
| private BasedSequence mClosingMarker = BasedSequence.EMPTY; | ||
| - private String mTypeName = ""; | ||
| - private String mIdName = ""; | ||
| + private BasedSequenceNameParser mParser; | ||
| - public AnchorNameNode( final Delimiter opener, final Delimiter closer ) {} | ||
| + public AnchorNameNode() {} | ||
| @Override | ||
| public String getTypeName() { | ||
| - return mTypeName; | ||
| + return mParser.getTypeName(); | ||
| } | ||
| @Override | ||
| public String getIdName() { | ||
| - return mIdName; | ||
| + return mParser.getIdName(); | ||
| } | ||
| @Override | ||
| public void setText( final BasedSequence text ) { | ||
| - final var matcher = PATTERN_ANCHOR.matcher( text.toString() ); | ||
| - | ||
| - if( matcher.find() ) { | ||
| - mTypeName = matcher.group( 1 ); | ||
| - mIdName = matcher.group( 2 ); | ||
| - } | ||
| + mParser = BasedSequenceNameParser.parse( text ); | ||
| } | ||
| -/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| - * | ||
| - * SPDX-License-Identifier: MIT | ||
| - */ | ||
| -package com.keenwrite.processors.markdown.extensions.references; | ||
| - | ||
| -import com.vladsch.flexmark.parser.InlineParserExtensionFactory; | ||
| -import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor; | ||
| -import com.vladsch.flexmark.parser.internal.InlineParserImpl; | ||
| -import com.vladsch.flexmark.parser.internal.LinkRefProcessorData; | ||
| -import com.vladsch.flexmark.util.data.DataHolder; | ||
| - | ||
| -import java.util.BitSet; | ||
| -import java.util.List; | ||
| -import java.util.Map; | ||
| -import java.util.regex.Pattern; | ||
| - | ||
| -import static java.util.regex.Pattern.compile; | ||
| - | ||
| -final class AnchorXrefInlineParser extends InlineParserImpl { | ||
| - | ||
| - private static final String REGEX_REFERENCE = "@(\\w+):(\\w+)"; | ||
| - private static final Pattern PATTERN_REFERENCE = compile( REGEX_REFERENCE ); | ||
| - | ||
| - private boolean mFoundReference; | ||
| - | ||
| - AnchorXrefInlineParser( | ||
| - final DataHolder options, | ||
| - final BitSet specialCharacters, | ||
| - final BitSet delimiterCharacters, | ||
| - final Map<Character, DelimiterProcessor> delimiterProcessors, | ||
| - final LinkRefProcessorData referenceLinkProcessors, | ||
| - final List<InlineParserExtensionFactory> inlineParserExtensions ) { | ||
| - super( | ||
| - options, | ||
| - specialCharacters, | ||
| - delimiterCharacters, | ||
| - delimiterProcessors, | ||
| - referenceLinkProcessors, | ||
| - inlineParserExtensions | ||
| - ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public boolean parseOpenBracket() { | ||
| - mFoundReference = peek( 1 ) == '@'; | ||
| - | ||
| - return super.parseOpenBracket(); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public boolean parseCloseBracket() { | ||
| - final boolean foundBracket = super.parseCloseBracket(); | ||
| - | ||
| - if( mFoundReference ) { | ||
| - final var blockNode = getBlock(); | ||
| - final var linkRef = blockNode.getLastChild(); | ||
| - | ||
| - if( linkRef != null ) { | ||
| - final var text = linkRef.getChildChars(); | ||
| - final var matcher = PATTERN_REFERENCE.matcher( text ); | ||
| - | ||
| - if( matcher.find() ) { | ||
| - final var typeName = matcher.group( 1 ); | ||
| - final var idName = matcher.group( 2 ); | ||
| - final var xref = new AnchorXrefNode( typeName, idName ); | ||
| - | ||
| - linkRef.unlink(); | ||
| - blockNode.appendChild( xref ); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - return foundBracket; | ||
| - } | ||
| -} | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.references; | ||
| + | ||
| +import com.vladsch.flexmark.parser.LinkRefProcessor; | ||
| +import com.vladsch.flexmark.parser.LinkRefProcessorFactory; | ||
| +import com.vladsch.flexmark.util.ast.Document; | ||
| +import com.vladsch.flexmark.util.ast.Node; | ||
| +import com.vladsch.flexmark.util.data.DataHolder; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| +import org.jetbrains.annotations.NotNull; | ||
| + | ||
| +/** | ||
| + * Responsible for processing {@code [@type:id]} anchors. | ||
| + */ | ||
| +class AnchorXrefProcessorFactory implements LinkRefProcessorFactory { | ||
| + private final LinkRefProcessor mProcessor = new AnchorLinkRefProcessor(); | ||
| + | ||
| + @Override | ||
| + public boolean getWantExclamationPrefix( @NotNull final DataHolder options ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public int getBracketNestingLevel( @NotNull final DataHolder options ) { | ||
| + return 0; | ||
| + } | ||
| + | ||
| + @NotNull | ||
| + @Override | ||
| + public LinkRefProcessor apply( @NotNull final Document document ) { | ||
| + return mProcessor; | ||
| + } | ||
| + | ||
| + private static class AnchorLinkRefProcessor implements LinkRefProcessor { | ||
| + | ||
| + @Override | ||
| + public boolean getWantExclamationPrefix() { | ||
| + return false; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public int getBracketNestingLevel() { | ||
| + return 0; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public boolean isMatch( @NotNull final BasedSequence nodeChars ) { | ||
| + return nodeChars.indexOf( '@' ) == 1; | ||
| + } | ||
| + | ||
| + @NotNull | ||
| + @Override | ||
| + public Node createNode( @NotNull final BasedSequence nodeChars ) { | ||
| + return BasedSequenceXrefParser.parse( nodeChars ).toNode(); | ||
| + } | ||
| + | ||
| + @NotNull | ||
| + @Override | ||
| + public BasedSequence adjustInlineText( | ||
| + @NotNull final Document document, | ||
| + @NotNull final Node node ) { | ||
| + return BasedSequence.EMPTY; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public boolean allowDelimiters( | ||
| + @NotNull final BasedSequence chars, | ||
| + @NotNull final Document document, | ||
| + @NotNull final Node node ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void updateNodeElements( | ||
| + @NotNull final Document document, | ||
| + @NotNull final Node node ) {} | ||
| + } | ||
| +} | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.references; | ||
| + | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| + | ||
| +import java.util.regex.Matcher; | ||
| +import java.util.regex.Pattern; | ||
| + | ||
| +import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; | ||
| +import static java.util.regex.Pattern.compile; | ||
| + | ||
| +class BasedSequenceNameParser extends BasedSequenceParser { | ||
| + private static final String REGEX = STR. "#\{ REGEX_INNER }" ; | ||
| + private static final Pattern PATTERN = compile( | ||
| + REGEX, UNICODE_CHARACTER_CLASS | ||
| + ); | ||
| + | ||
| + private BasedSequenceNameParser( final String text ) { | ||
| + super( text ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + Matcher createMatcher( final String text ) { | ||
| + return PATTERN.matcher( text ); | ||
| + } | ||
| + | ||
| + static BasedSequenceNameParser parse( final BasedSequence chars ) { | ||
| + return new BasedSequenceNameParser( chars.toString() ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.references; | ||
| + | ||
| +import java.util.regex.Matcher; | ||
| + | ||
| +abstract class BasedSequenceParser { | ||
| + /** | ||
| + * Shared syntax between subclasses. | ||
| + */ | ||
| + static final String REGEX_INNER = "(\\p{Alnum}+):(\\p{Alnum}+)"; | ||
| + | ||
| + private final String mTypeName; | ||
| + private final String mIdName; | ||
| + | ||
| + BasedSequenceParser( final String text ) { | ||
| + final var matcher = createMatcher( text ); | ||
| + | ||
| + if( matcher.find() ) { | ||
| + mTypeName = matcher.group( 1 ); | ||
| + mIdName = matcher.group( 2 ); | ||
| + } | ||
| + else { | ||
| + mTypeName = null; | ||
| + mIdName = null; | ||
| + } | ||
| + } | ||
| + | ||
| + abstract Matcher createMatcher( final String text ); | ||
| + | ||
| + String getTypeName() { | ||
| + return mTypeName; | ||
| + } | ||
| + | ||
| + String getIdName() { | ||
| + return mIdName; | ||
| + } | ||
| +} | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.references; | ||
| + | ||
| +import com.vladsch.flexmark.util.ast.Node; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| + | ||
| +import java.util.regex.Matcher; | ||
| +import java.util.regex.Pattern; | ||
| + | ||
| +import static com.keenwrite.processors.markdown.extensions.EmptyNode.EMPTY_NODE; | ||
| +import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; | ||
| +import static java.util.regex.Pattern.compile; | ||
| + | ||
| +class BasedSequenceXrefParser extends BasedSequenceParser { | ||
| + private static final String REGEX = STR. "\\[@\{ REGEX_INNER }]" ; | ||
| + private static final Pattern PATTERN = compile( | ||
| + REGEX, UNICODE_CHARACTER_CLASS | ||
| + ); | ||
| + | ||
| + BasedSequenceXrefParser( final String text ) { | ||
| + super( text ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + Matcher createMatcher( final String text ) { | ||
| + return PATTERN.matcher( text ); | ||
| + } | ||
| + | ||
| + static BasedSequenceXrefParser parse( final BasedSequence chars ) { | ||
| + return new BasedSequenceXrefParser( chars.toString() ); | ||
| + } | ||
| + | ||
| + Node toNode() { | ||
| + final var typeName = getTypeName(); | ||
| + final var idName = getIdName(); | ||
| + | ||
| + return typeName == null || idName == null | ||
| + ? EMPTY_NODE | ||
| + : new AnchorXrefNode( typeName, idName ); | ||
| + } | ||
| +} | ||
| @Override | ||
| public void extend( final Parser.Builder builder ) { | ||
| - builder.customInlineParserFactory( AnchorXrefInlineParser::new ); | ||
| + builder.linkRefProcessorFactory( new AnchorXrefProcessorFactory() ); | ||
| builder.customDelimiterProcessor( new AnchorNameDelimiterProcessor() ); | ||
| } |
| String getRefAttrName(); | ||
| - default String toHtml() { | ||
| - final String typeName = getTypeName(); | ||
| - final String idName = getIdName(); | ||
| - | ||
| - return toHtml( typeName, idName ); | ||
| - } | ||
| - | ||
| - default String toHtml( final String type, final String id ) { | ||
| - return STR. | ||
| - "<a data-type=\"\{ type }\" \{ getRefAttrName() }=\"\{ id }\" />" ; | ||
| - } | ||
| - | ||
| + /** | ||
| + * Writes the HTML representation for this cross-reference node. | ||
| + * | ||
| + * @param html The HTML tag is written to the {@link HtmlWriter}. | ||
| + */ | ||
| default void write( final HtmlWriter html ) { | ||
| - html.raw( toHtml() ); | ||
| + final var type = getTypeName(); | ||
| + final var id = getIdName(); | ||
| + final var attr = getRefAttrName(); | ||
| + | ||
| + html.raw( STR. "<a data-type=\"\{ type }\" \{ attr }=\"\{ id }\" />" ); | ||
| } | ||
| } |