| */ | ||
| public final class XhtmlProcessor extends ExecutorProcessor<String> { | ||
| - private static final Curler sTypographer = | ||
| + private static final Curler sCurler = | ||
| new Curler( createContractions(), FILTER_XML, true ); | ||
| final var curl = mContext.getCurlQuotes(); | ||
| - return curl ? sTypographer.apply( document ) : document; | ||
| + return curl ? sCurler.apply( document ) : document; | ||
| } catch( final Exception ex ) { | ||
| clue( ex ); | ||
| import com.keenwrite.processors.variable.VariableProcessor; | ||
| import com.keenwrite.processors.markdown.extensions.caret.CaretExtension; | ||
| -import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.ImageBlockExtension; | ||
| import com.keenwrite.processors.markdown.extensions.images.ImageLinkExtension; | ||
| import com.keenwrite.processors.markdown.extensions.outline.DocumentOutlineExtension; | ||
| result.add( ImageLinkExtension.create( context ) ); | ||
| result.add( TexExtension.create( evaluator, context ) ); | ||
| - result.add( FencedBlockExtension.create( processor, evaluator, context ) ); | ||
| + result.add( ImageBlockExtension.create( processor, evaluator, context ) ); | ||
| if( context.isExportFormat( ExportFormat.NONE ) ) { | ||
| package com.keenwrite.processors.markdown.extensions.captions; | ||
| -import com.keenwrite.processors.markdown.extensions.fences.ClosingDivBlock; | ||
| -import com.keenwrite.processors.markdown.extensions.fences.OpeningDivBlock; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock; | ||
| import com.vladsch.flexmark.parser.block.NodePostProcessor; | ||
| import com.vladsch.flexmark.util.ast.Node; |
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors.markdown.extensions.fences; | ||
| - | ||
| -import com.vladsch.flexmark.html.HtmlWriter; | ||
| - | ||
| -/** | ||
| - * Responsible for helping to generate a closing {@code div} element. | ||
| - */ | ||
| -public final class ClosingDivBlock extends DivBlock { | ||
| - @Override | ||
| - void write( final HtmlWriter html ) { | ||
| - html.closeTag( HTML_DIV ); | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors.markdown.extensions.fences; | ||
| - | ||
| -import com.vladsch.flexmark.html.HtmlWriter; | ||
| -import com.vladsch.flexmark.util.ast.Block; | ||
| -import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| -import org.jetbrains.annotations.NotNull; | ||
| - | ||
| -abstract class DivBlock extends Block { | ||
| - static final CharSequence HTML_DIV = "div"; | ||
| - | ||
| - @Override | ||
| - @NotNull | ||
| - public BasedSequence[] getSegments() { | ||
| - return EMPTY_SEGMENTS; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Append an opening or closing HTML div element to the given writer. | ||
| - * | ||
| - * @param html Builds the HTML document to be written. | ||
| - */ | ||
| - abstract void write( HtmlWriter html ); | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors.markdown.extensions.fences; | ||
| - | ||
| -import com.keenwrite.preview.DiagramUrlGenerator; | ||
| -import com.keenwrite.processors.Processor; | ||
| -import com.keenwrite.processors.ProcessorContext; | ||
| -import com.keenwrite.processors.variable.VariableProcessor; | ||
| -import com.keenwrite.processors.markdown.MarkdownProcessor; | ||
| -import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter; | ||
| -import com.keenwrite.processors.r.RChunkEvaluator; | ||
| -import com.keenwrite.processors.variable.RVariableProcessor; | ||
| -import com.vladsch.flexmark.ast.FencedCodeBlock; | ||
| -import com.vladsch.flexmark.html.HtmlRendererOptions; | ||
| -import com.vladsch.flexmark.html.HtmlWriter; | ||
| -import com.vladsch.flexmark.html.renderer.*; | ||
| -import com.vladsch.flexmark.util.data.DataHolder; | ||
| -import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| -import com.whitemagicsoftware.keenquotes.util.Tuple; | ||
| -import org.jetbrains.annotations.NotNull; | ||
| - | ||
| -import java.nio.file.Path; | ||
| -import java.util.HashSet; | ||
| -import java.util.Set; | ||
| -import java.util.function.Function; | ||
| - | ||
| -import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | ||
| -import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY; | ||
| -import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| -import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | ||
| -import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT; | ||
| -import static com.vladsch.flexmark.html.renderer.LinkType.LINK; | ||
| -import static java.lang.String.format; | ||
| - | ||
| -/** | ||
| - * Responsible for converting textual diagram descriptions into HTML image | ||
| - * elements. | ||
| - */ | ||
| -public final class FencedBlockExtension extends HtmlRendererAdapter { | ||
| - /** | ||
| - * Ensure that the device is always closed to prevent an out-of-resources | ||
| - * error, regardless of whether the R expression the user tries to evaluate | ||
| - * is valid by swallowing errors alongside a {@code finally} block. | ||
| - */ | ||
| - private static final String R_SVG_EXPORT = | ||
| - "tryCatch({svg('%s'%s)%n%s%n},finally={dev.off()})%n"; | ||
| - | ||
| - private static final String STYLE_DIAGRAM = "diagram-"; | ||
| - private static final int STYLE_DIAGRAM_LEN = STYLE_DIAGRAM.length(); | ||
| - | ||
| - private static final String STYLE_R_CHUNK = "{r"; | ||
| - | ||
| - private static final class VerbatimRVariableProcessor | ||
| - extends RVariableProcessor { | ||
| - | ||
| - public VerbatimRVariableProcessor( | ||
| - final Processor<String> successor, final ProcessorContext context ) { | ||
| - super( successor, context ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - protected String processValue( final String value ) { | ||
| - return value; | ||
| - } | ||
| - } | ||
| - | ||
| - private final RChunkEvaluator mRChunkEvaluator; | ||
| - private final Function<String, String> mInlineEvaluator; | ||
| - | ||
| - private final Processor<String> mRVariableProcessor; | ||
| - private final ProcessorContext mContext; | ||
| - | ||
| - public FencedBlockExtension( | ||
| - final Processor<String> processor, | ||
| - final Function<String, String> evaluator, | ||
| - final ProcessorContext context ) { | ||
| - assert processor != null; | ||
| - assert context != null; | ||
| - mContext = context; | ||
| - mRChunkEvaluator = new RChunkEvaluator(); | ||
| - mInlineEvaluator = evaluator; | ||
| - mRVariableProcessor = new VerbatimRVariableProcessor( IDENTITY, context ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates a new parser for fenced blocks. This calls out to a web service | ||
| - * to generate SVG files of text diagrams. | ||
| - * <p> | ||
| - * Internally, this creates a {@link VariableProcessor} to substitute | ||
| - * variable definitions. This is necessary because the order of processors | ||
| - * matters. If the {@link VariableProcessor} comes before an instance of | ||
| - * {@link MarkdownProcessor}, for example, then the caret position in the | ||
| - * preview pane will not align with the caret position in the editor | ||
| - * pane. The {@link MarkdownProcessor} must come before all else. However, | ||
| - * when parsing fenced blocks, the variables within the block must be | ||
| - * interpolated before being sent to the diagram web service. | ||
| - * </p> | ||
| - * | ||
| - * @param processor Used to pre-process the text. | ||
| - * @return A new {@link FencedBlockExtension} capable of shunting ASCII | ||
| - * diagrams to a service for conversion to SVG. | ||
| - */ | ||
| - public static FencedBlockExtension create( | ||
| - final Processor<String> processor, | ||
| - final Function<String, String> evaluator, | ||
| - final ProcessorContext context ) { | ||
| - assert processor != null; | ||
| - assert context != null; | ||
| - return new FencedBlockExtension( processor, evaluator, context ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void extend( | ||
| - @NotNull final Builder builder, @NotNull final String rendererType ) { | ||
| - builder.nodeRendererFactory( new Factory() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Converts the given {@link BasedSequence} to a lowercase value. | ||
| - * | ||
| - * @param text The character string to convert to lowercase. | ||
| - * @return The lowercase text value, or the empty string for no text. | ||
| - */ | ||
| - private static String sanitize( final BasedSequence text ) { | ||
| - assert text != null; | ||
| - return text.toString().toLowerCase(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Responsible for generating images from a fenced block that contains a | ||
| - * diagram reference. | ||
| - */ | ||
| - private class CustomRenderer implements NodeRenderer { | ||
| - | ||
| - @Override | ||
| - public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | ||
| - final var set = new HashSet<NodeRenderingHandler<?>>(); | ||
| - | ||
| - set.add( new NodeRenderingHandler<>( | ||
| - FencedCodeBlock.class, ( node, context, html ) -> { | ||
| - final var style = sanitize( node.getInfo() ); | ||
| - final Tuple<String, ResolvedLink> imagePair; | ||
| - | ||
| - if( style.startsWith( STYLE_DIAGRAM ) ) { | ||
| - imagePair = importTextDiagram( style, node, context ); | ||
| - | ||
| - html.attr( "src", imagePair.item1() ); | ||
| - html.withAttr( imagePair.item2() ); | ||
| - html.tagVoid( "img" ); | ||
| - } | ||
| - else if( style.startsWith( STYLE_R_CHUNK ) ) { | ||
| - imagePair = evaluateRChunk( node, context ); | ||
| - | ||
| - html.attr( "src", imagePair.item1() ); | ||
| - html.withAttr( imagePair.item2() ); | ||
| - html.tagVoid( "img" ); | ||
| - } | ||
| - else { | ||
| - // TODO: Revert to using context.delegateRender() after flexmark | ||
| - // is updated to no longer trim blank lines up to the EOL. | ||
| - render( node, context, html ); | ||
| - } | ||
| - } ) ); | ||
| - | ||
| - return set; | ||
| - } | ||
| - | ||
| - private Tuple<String, ResolvedLink> importTextDiagram( | ||
| - final String style, | ||
| - final FencedCodeBlock node, | ||
| - final NodeRendererContext context ) { | ||
| - | ||
| - final var type = style.substring( STYLE_DIAGRAM_LEN ); | ||
| - final var content = node.getContentChars().normalizeEOL(); | ||
| - final var text = mInlineEvaluator.apply( content ); | ||
| - final var server = mContext.getImageServer(); | ||
| - final var source = DiagramUrlGenerator.toUrl( server, type, text ); | ||
| - final var link = context.resolveLink( LINK, source, false ); | ||
| - | ||
| - return new Tuple<>( source, link ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Evaluates an R expression. This will take into consideration any | ||
| - * key/value pairs passed in from the document, such as width and height | ||
| - * attributes of the form: <code>{r width=5 height=5}</code>. | ||
| - * | ||
| - * @param node The {@link FencedCodeBlock} to evaluate using R. | ||
| - * @param context Used to resolve the link that refers to any resulting | ||
| - * image produced by the R chunk (such as a plot). | ||
| - * @return The SVG text string associated with the content produced by | ||
| - * the chunk (such as a graphical data plot). | ||
| - */ | ||
| - @SuppressWarnings( "unused" ) | ||
| - private Tuple<String, ResolvedLink> evaluateRChunk( | ||
| - final FencedCodeBlock node, | ||
| - final NodeRendererContext context ) { | ||
| - final var content = node.getContentChars().normalizeEOL().trim(); | ||
| - final var text = mRVariableProcessor.apply( content ); | ||
| - final var hash = Integer.toHexString( text.hashCode() ); | ||
| - final var filename = format( "%s-%s.svg", APP_TITLE_LOWERCASE, hash ); | ||
| - | ||
| - // The URI helps convert backslashes to forward slashes. | ||
| - final var uri = Path.of( TEMPORARY_DIRECTORY, filename ).toUri(); | ||
| - final var svg = uri.getPath(); | ||
| - final var link = context.resolveLink( LINK, svg, false ); | ||
| - final var dimensions = getAttributes( node.getInfo() ); | ||
| - final var r = format( R_SVG_EXPORT, svg, dimensions, text ); | ||
| - | ||
| - mRChunkEvaluator.apply( r ); | ||
| - | ||
| - return new Tuple<>( svg, link ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Splits attributes of the form <code>{r key1=value2 key2=value2}</code> | ||
| - * into a comma-separated string containing only the key/value pairs, | ||
| - * such as <code>key1=value1,key2=value2</code>. | ||
| - * | ||
| - * @param bs The complete line after the fenced block demarcation. | ||
| - * @return A comma-separated string of name/value pairs. | ||
| - */ | ||
| - private String getAttributes( final BasedSequence bs ) { | ||
| - final var result = new StringBuilder(); | ||
| - final var split = bs.splitList( " " ); | ||
| - final var splits = split.size(); | ||
| - | ||
| - for( var i = 1; i < splits; i++ ) { | ||
| - final var based = split.get( i ).toString(); | ||
| - final var attribute = based.replace( '}', ' ' ); | ||
| - | ||
| - // The order of attribute evaluations is in order of performance. | ||
| - if( !attribute.isBlank() && | ||
| - attribute.indexOf( '=' ) > 1 && | ||
| - attribute.matches( ".*\\d.*" ) ) { | ||
| - | ||
| - // The comma will do double-duty for separating individual attributes | ||
| - // as well as being the comma that separates all attributes from the | ||
| - // SVG image file name. | ||
| - result.append( ',' ).append( attribute ); | ||
| - } | ||
| - } | ||
| - | ||
| - return result.toString(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * This method is a stop-gap because blank lines that contain only | ||
| - * whitespace are collapsed into lines without any spaces. Consequently, | ||
| - * the typesetting software does not honour the blank lines, which | ||
| - * then would otherwise discard blank lines entirely. | ||
| - * <p> | ||
| - * Given the following: | ||
| - * | ||
| - * <pre> | ||
| - * if( bool ) { | ||
| - * | ||
| - * | ||
| - * } | ||
| - * </pre> | ||
| - * <p> | ||
| - * The typesetter would otherwise render this incorrectly as: | ||
| - * | ||
| - * <pre> | ||
| - * if( bool ) { | ||
| - * } | ||
| - * </pre> | ||
| - * <p> | ||
| - */ | ||
| - private void render( | ||
| - final FencedCodeBlock node, | ||
| - final NodeRendererContext context, | ||
| - final HtmlWriter html ) { | ||
| - assert node != null; | ||
| - assert context != null; | ||
| - assert html != null; | ||
| - | ||
| - html.line(); | ||
| - html.srcPosWithTrailingEOL( node.getChars() ) | ||
| - .withAttr() | ||
| - .tag( "pre" ) | ||
| - .openPre(); | ||
| - | ||
| - final var options = context.getHtmlOptions(); | ||
| - final var languageClass = lookupLanguageClass( node, options ); | ||
| - | ||
| - if( !languageClass.isBlank() ) { | ||
| - html.attr( "class", languageClass ); | ||
| - } | ||
| - | ||
| - html.srcPosWithEOL( node.getContentChars() ) | ||
| - .withAttr( CODE_CONTENT ) | ||
| - .tag( "code" ); | ||
| - | ||
| - final var lines = node.getContentLines(); | ||
| - | ||
| - for( final var line : lines ) { | ||
| - if( line.isBlank() ) { | ||
| - html.text( " " ); | ||
| - } | ||
| - | ||
| - html.text( line ); | ||
| - } | ||
| - | ||
| - html.tag( "/code" ); | ||
| - html.tag( "/pre" ) | ||
| - .closePre(); | ||
| - html.lineIf( options.htmlBlockCloseTagEol ); | ||
| - } | ||
| - | ||
| - private String lookupLanguageClass( | ||
| - final FencedCodeBlock node, | ||
| - final HtmlRendererOptions options ) { | ||
| - assert node != null; | ||
| - assert options != null; | ||
| - | ||
| - final var info = node.getInfo(); | ||
| - | ||
| - if( info.isNotNull() && !info.isBlank() ) { | ||
| - final var lang = node | ||
| - .getInfoDelimitedByAny( options.languageDelimiterSet ) | ||
| - .unescape(); | ||
| - return options | ||
| - .languageClassMap | ||
| - .getOrDefault( lang, options.languageClassPrefix + lang ); | ||
| - } | ||
| - | ||
| - return options.noLanguageClass; | ||
| - } | ||
| - } | ||
| - | ||
| - private class Factory implements DelegatingNodeRendererFactory { | ||
| - public Factory() { } | ||
| - | ||
| - @NotNull | ||
| - @Override | ||
| - public NodeRenderer apply( @NotNull final DataHolder options ) { | ||
| - return new CustomRenderer(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Return {@code null} to indicate this may delegate to the core renderer. | ||
| - */ | ||
| - @Override | ||
| - public Set<Class<?>> getDelegates() { | ||
| - return null; | ||
| - } | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. */ | ||
| package com.keenwrite.processors.markdown.extensions.fences; | ||
| -import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory; | ||
| +import com.keenwrite.processors.markdown.extensions.common.MarkdownParserExtension; | ||
| import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.factories.CustomDivBlockParserFactory; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.factories.FencedDivNodeRendererFactory; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.factories.FencedDivPreProcessorFactory; | ||
| import com.vladsch.flexmark.html.renderer.NodeRendererFactory; | ||
| import com.vladsch.flexmark.parser.Parser.Builder; | ||
| -import com.vladsch.flexmark.parser.block.*; | ||
| -import com.vladsch.flexmark.util.ast.Block; | ||
| -import com.vladsch.flexmark.util.data.DataHolder; | ||
| -import com.vladsch.flexmark.util.html.Attribute; | ||
| -import com.vladsch.flexmark.util.html.AttributeImpl; | ||
| -import java.util.ArrayList; | ||
| import java.util.regex.Pattern; | ||
| * </p> | ||
| * <p> | ||
| - * ::: {#verse .p .d k=v author="Emily Dickinson"} | ||
| + * ::: {#verse .p .d k=v author="Dickinson"} | ||
| * Because I could not stop for Death -- | ||
| * He kindly stopped for me -- | ||
| * The second example produces the following starting {@code div} element: | ||
| * </p> | ||
| + * {@literal <div id="verse" class="p d" data-k="v" data-author="Dickson">} | ||
| + * | ||
| * <p> | ||
| - * <div id="verse" class="p d" data-k="v" data-author="Emily Dickson"> | ||
| + * This will parse fenced divs embedded inside of blockquote environments. | ||
| * </p> | ||
| */ | ||
| -public class FencedDivExtension extends MarkdownRendererExtension { | ||
| +public class FencedDivExtension extends MarkdownRendererExtension | ||
| + implements MarkdownParserExtension { | ||
| /** | ||
| * Matches any number of colons at start of line. This will match both the | ||
| * opening and closing fences, with any number of colons. | ||
| */ | ||
| - private static final Pattern FENCE = compile( "^:::.*" ); | ||
| + public static final Pattern FENCE = compile( "^:::+.*" ); | ||
| /** | ||
| * After a fenced div is detected, this will match the opening fence. | ||
| - */ | ||
| - private static final Pattern FENCE_OPENING = compile( | ||
| - "^:::+\\s+([\\p{Alnum}-_]+|\\{.+})\\s*$", | ||
| - UNICODE_CHARACTER_CLASS ); | ||
| - | ||
| - /** | ||
| - * Matches whether extended syntax is being used. | ||
| - */ | ||
| - private static final Pattern ATTR_CSS = compile( "\\{(.+)}" ); | ||
| - | ||
| - /** | ||
| - * Matches either individual CSS definitions (id/class, {@code <d>}) or | ||
| - * key/value pairs ({@code <k>} and {@link <v>}). The key/value pair | ||
| - * will match optional quotes. | ||
| */ | ||
| - private static final Pattern ATTR_PAIRS = compile( | ||
| - "\\s*" + | ||
| - "(?<d>[#.][\\p{Alnum}-_]+[^\\s=])|" + | ||
| - "((?<k>[\\p{Alnum}-_]+)=" + | ||
| - "\"*(?<v>(?<=\")[^\"]+(?=\")|(\\S+))\"*)", | ||
| + public static final Pattern FENCE_OPENING = compile( | ||
| + "^:::+\\s+([\\p{Alnum}\\-_]+|\\{.+})\\s*$", | ||
| UNICODE_CHARACTER_CLASS ); | ||
| public static FencedDivExtension create() { | ||
| return new FencedDivExtension(); | ||
| } | ||
| @Override | ||
| public void extend( final Builder builder ) { | ||
| - builder.customBlockParserFactory( new DivBlockParserFactory() ); | ||
| + builder.customBlockParserFactory( new CustomDivBlockParserFactory() ); | ||
| + builder.paragraphPreProcessorFactory( new FencedDivPreProcessorFactory() ); | ||
| } | ||
| @Override | ||
| protected NodeRendererFactory createNodeRendererFactory() { | ||
| return new FencedDivNodeRendererFactory(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Responsible for creating an instance of {@link ParserFactory}. | ||
| - */ | ||
| - private static class DivBlockParserFactory | ||
| - extends MarkdownCustomBlockParserFactory { | ||
| - @Override | ||
| - public BlockParserFactory createBlockParserFactory( final DataHolder options ) { | ||
| - return new ParserFactory( options ); | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Responsible for creating a fenced div parser that is appropriate for the | ||
| - * type of fenced div encountered: opening or closing. | ||
| - */ | ||
| - private static class ParserFactory extends AbstractBlockParserFactory { | ||
| - public ParserFactory( final DataHolder options ) { | ||
| - super( options ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Try to match an opening or closing fenced div. | ||
| - * | ||
| - * @param state Block parser state. | ||
| - * @param matchedBlockParser Last matched open block parser. | ||
| - * @return Wrapper for the opening or closing parser, upon finding :::. | ||
| - */ | ||
| - @Override | ||
| - public BlockStart tryStart( | ||
| - final ParserState state, final MatchedBlockParser matchedBlockParser ) { | ||
| - return | ||
| - state.getIndent() == 0 && FENCE.matcher( state.getLine() ).matches() | ||
| - ? parseFence( state ) | ||
| - : BlockStart.none(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * After finding a fenced div, this will further disambiguate an opening | ||
| - * from a closing fence. | ||
| - * | ||
| - * @param state Block parser state, contains line to parse. | ||
| - * @return Wrapper for the opening or closing parser, upon finding :::. | ||
| - */ | ||
| - private BlockStart parseFence( final ParserState state ) { | ||
| - final var fence = FENCE_OPENING.matcher( state.getLine() ); | ||
| - | ||
| - return BlockStart.of( | ||
| - fence.matches() | ||
| - ? new OpeningParser( fence.group( 1 ) ) | ||
| - : new ClosingParser() | ||
| - ).atIndex( state.getIndex() ); | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Abstracts common {@link OpeningParser} and {@link ClosingParser} methods. | ||
| - */ | ||
| - private static abstract class DivBlockParser extends AbstractBlockParser { | ||
| - @Override | ||
| - public BlockContinue tryContinue( final ParserState state ) { | ||
| - return BlockContinue.none(); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public void closeBlock( final ParserState state ) {} | ||
| - } | ||
| - | ||
| - /** | ||
| - * Responsible for creating an instance of {@link OpeningDivBlock}. | ||
| - */ | ||
| - private static class OpeningParser extends DivBlockParser { | ||
| - private final OpeningDivBlock mBlock; | ||
| - | ||
| - /** | ||
| - * Parses the arguments upon construction. | ||
| - * | ||
| - * @param args Text after :::, excluding leading/trailing whitespace. | ||
| - */ | ||
| - public OpeningParser( final String args ) { | ||
| - final var attrs = new ArrayList<Attribute>(); | ||
| - final var cssMatcher = ATTR_CSS.matcher( args ); | ||
| - | ||
| - if( cssMatcher.matches() ) { | ||
| - // Split the text between braces into tokens and/or key-value pairs. | ||
| - final var pairMatcher = ATTR_PAIRS.matcher( cssMatcher.group( 1 ) ); | ||
| - | ||
| - while( pairMatcher.find() ) { | ||
| - final var cssDef = pairMatcher.group( "d" ); | ||
| - String cssAttrKey = "class"; | ||
| - String cssAttrVal; | ||
| - | ||
| - // When no regular CSS definition (id or class), use key/value pairs. | ||
| - if( cssDef == null ) { | ||
| - cssAttrKey = "data-" + pairMatcher.group( "k" ); | ||
| - cssAttrVal = pairMatcher.group( "v" ); | ||
| - } | ||
| - else { | ||
| - // This will strip the "#" and "." off the start of CSS definition. | ||
| - var index = 1; | ||
| - | ||
| - // Default CSS attribute name is "class", switch to "id" for #. | ||
| - if( cssDef.startsWith( "#" ) ) { | ||
| - cssAttrKey = "id"; | ||
| - } | ||
| - else if( !cssDef.startsWith( "." ) ) { | ||
| - index = 0; | ||
| - } | ||
| - | ||
| - cssAttrVal = cssDef.substring( index ); | ||
| - } | ||
| - | ||
| - attrs.add( AttributeImpl.of( cssAttrKey, cssAttrVal ) ); | ||
| - } | ||
| - } | ||
| - else { | ||
| - attrs.add( AttributeImpl.of( "class", args ) ); | ||
| - } | ||
| - | ||
| - mBlock = new OpeningDivBlock( attrs ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - public Block getBlock() { | ||
| - return mBlock; | ||
| - } | ||
| - } | ||
| - | ||
| - /** | ||
| - * Responsible for creating an instance of {@link ClosingDivBlock}. | ||
| - */ | ||
| - private static class ClosingParser extends DivBlockParser { | ||
| - private final ClosingDivBlock mBlock = new ClosingDivBlock(); | ||
| - | ||
| - @Override | ||
| - public Block getBlock() { | ||
| - return mBlock; | ||
| - } | ||
| } | ||
| } | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors.markdown.extensions.fences; | ||
| - | ||
| -import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory; | ||
| -import com.vladsch.flexmark.html.renderer.NodeRenderer; | ||
| -import com.vladsch.flexmark.util.data.DataHolder; | ||
| - | ||
| -class FencedDivNodeRendererFactory extends MarkdownNodeRendererFactory { | ||
| - @Override | ||
| - protected NodeRenderer createNodeRenderer( final DataHolder options ) { | ||
| - return new FencedDivRenderer(); | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors.markdown.extensions.fences; | ||
| - | ||
| -import com.vladsch.flexmark.html.HtmlWriter; | ||
| -import com.vladsch.flexmark.html.renderer.NodeRenderer; | ||
| -import com.vladsch.flexmark.html.renderer.NodeRendererContext; | ||
| -import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | ||
| -import org.jetbrains.annotations.Nullable; | ||
| - | ||
| -import java.util.Set; | ||
| - | ||
| -/** | ||
| - * Responsible for rendering opening and closing fenced div blocks as HTML | ||
| - * {@code div} elements. | ||
| - */ | ||
| -class FencedDivRenderer implements NodeRenderer { | ||
| - @Nullable | ||
| - @Override | ||
| - public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | ||
| - return Set.of( | ||
| - new NodeRenderingHandler<>( OpeningDivBlock.class, this::render ), | ||
| - new NodeRenderingHandler<>( ClosingDivBlock.class, this::render ) | ||
| - ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Renders the fenced div block as an HTML {@code <div></div>} element. | ||
| - */ | ||
| - void render( | ||
| - final DivBlock node, | ||
| - final NodeRendererContext context, | ||
| - final HtmlWriter html ) { | ||
| - node.write( html ); | ||
| - } | ||
| -} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences; | ||
| + | ||
| +import com.keenwrite.preview.DiagramUrlGenerator; | ||
| +import com.keenwrite.processors.Processor; | ||
| +import com.keenwrite.processors.ProcessorContext; | ||
| +import com.keenwrite.processors.variable.VariableProcessor; | ||
| +import com.keenwrite.processors.markdown.MarkdownProcessor; | ||
| +import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter; | ||
| +import com.keenwrite.processors.r.RChunkEvaluator; | ||
| +import com.keenwrite.processors.variable.RVariableProcessor; | ||
| +import com.vladsch.flexmark.ast.FencedCodeBlock; | ||
| +import com.vladsch.flexmark.html.HtmlRendererOptions; | ||
| +import com.vladsch.flexmark.html.HtmlWriter; | ||
| +import com.vladsch.flexmark.html.renderer.*; | ||
| +import com.vladsch.flexmark.util.data.DataHolder; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| +import com.whitemagicsoftware.keenquotes.util.Tuple; | ||
| +import org.jetbrains.annotations.NotNull; | ||
| + | ||
| +import java.nio.file.Path; | ||
| +import java.util.HashSet; | ||
| +import java.util.Set; | ||
| +import java.util.function.Function; | ||
| + | ||
| +import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | ||
| +import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| +import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | ||
| +import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT; | ||
| +import static com.vladsch.flexmark.html.renderer.LinkType.LINK; | ||
| +import static java.lang.String.format; | ||
| + | ||
| +/** | ||
| + * Responsible for converting textual diagram descriptions into HTML image | ||
| + * elements. | ||
| + */ | ||
| +public final class ImageBlockExtension extends HtmlRendererAdapter { | ||
| + /** | ||
| + * Ensure that the device is always closed to prevent an out-of-resources | ||
| + * error, regardless of whether the R expression the user tries to evaluate | ||
| + * is valid by swallowing errors alongside a {@code finally} block. | ||
| + */ | ||
| + private static final String R_SVG_EXPORT = | ||
| + "tryCatch({svg('%s'%s)%n%s%n},finally={dev.off()})%n"; | ||
| + | ||
| + private static final String STYLE_DIAGRAM = "diagram-"; | ||
| + private static final int STYLE_DIAGRAM_LEN = STYLE_DIAGRAM.length(); | ||
| + | ||
| + private static final String STYLE_R_CHUNK = "{r"; | ||
| + | ||
| + private static final class VerbatimRVariableProcessor | ||
| + extends RVariableProcessor { | ||
| + | ||
| + public VerbatimRVariableProcessor( | ||
| + final Processor<String> successor, final ProcessorContext context ) { | ||
| + super( successor, context ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected String processValue( final String value ) { | ||
| + return value; | ||
| + } | ||
| + } | ||
| + | ||
| + private final RChunkEvaluator mRChunkEvaluator; | ||
| + private final Function<String, String> mInlineEvaluator; | ||
| + | ||
| + private final Processor<String> mRVariableProcessor; | ||
| + private final ProcessorContext mContext; | ||
| + | ||
| + public ImageBlockExtension( | ||
| + final Processor<String> processor, | ||
| + final Function<String, String> evaluator, | ||
| + final ProcessorContext context ) { | ||
| + assert processor != null; | ||
| + assert context != null; | ||
| + mContext = context; | ||
| + mRChunkEvaluator = new RChunkEvaluator(); | ||
| + mInlineEvaluator = evaluator; | ||
| + mRVariableProcessor = new VerbatimRVariableProcessor( IDENTITY, context ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates a new parser for fenced blocks. This calls out to a web service | ||
| + * to generate SVG files of text diagrams. | ||
| + * <p> | ||
| + * Internally, this creates a {@link VariableProcessor} to substitute | ||
| + * variable definitions. This is necessary because the order of processors | ||
| + * matters. If the {@link VariableProcessor} comes before an instance of | ||
| + * {@link MarkdownProcessor}, for example, then the caret position in the | ||
| + * preview pane will not align with the caret position in the editor | ||
| + * pane. The {@link MarkdownProcessor} must come before all else. However, | ||
| + * when parsing fenced blocks, the variables within the block must be | ||
| + * interpolated before being sent to the diagram web service. | ||
| + * </p> | ||
| + * | ||
| + * @param processor Used to pre-process the text. | ||
| + * @return A new {@link ImageBlockExtension} capable of shunting ASCII | ||
| + * diagrams to a service for conversion to SVG. | ||
| + */ | ||
| + public static ImageBlockExtension create( | ||
| + final Processor<String> processor, | ||
| + final Function<String, String> evaluator, | ||
| + final ProcessorContext context ) { | ||
| + assert processor != null; | ||
| + assert context != null; | ||
| + return new ImageBlockExtension( processor, evaluator, context ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void extend( | ||
| + @NotNull final Builder builder, @NotNull final String rendererType ) { | ||
| + builder.nodeRendererFactory( new Factory() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Converts the given {@link BasedSequence} to a lowercase value. | ||
| + * | ||
| + * @param text The character string to convert to lowercase. | ||
| + * @return The lowercase text value, or the empty string for no text. | ||
| + */ | ||
| + private static String sanitize( final BasedSequence text ) { | ||
| + assert text != null; | ||
| + return text.toString().toLowerCase(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Responsible for generating images from a fenced block that contains a | ||
| + * diagram reference. | ||
| + */ | ||
| + private class CustomRenderer implements NodeRenderer { | ||
| + | ||
| + @Override | ||
| + public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | ||
| + final var set = new HashSet<NodeRenderingHandler<?>>(); | ||
| + | ||
| + set.add( new NodeRenderingHandler<>( | ||
| + FencedCodeBlock.class, ( node, context, html ) -> { | ||
| + final var style = sanitize( node.getInfo() ); | ||
| + final Tuple<String, ResolvedLink> imagePair; | ||
| + | ||
| + if( style.startsWith( STYLE_DIAGRAM ) ) { | ||
| + imagePair = importTextDiagram( style, node, context ); | ||
| + | ||
| + html.attr( "src", imagePair.item1() ); | ||
| + html.withAttr( imagePair.item2() ); | ||
| + html.tagVoid( "img" ); | ||
| + } | ||
| + else if( style.startsWith( STYLE_R_CHUNK ) ) { | ||
| + imagePair = evaluateRChunk( node, context ); | ||
| + | ||
| + html.attr( "src", imagePair.item1() ); | ||
| + html.withAttr( imagePair.item2() ); | ||
| + html.tagVoid( "img" ); | ||
| + } | ||
| + else { | ||
| + // TODO: Revert to using context.delegateRender() after flexmark | ||
| + // is updated to no longer trim blank lines up to the EOL. | ||
| + render( node, context, html ); | ||
| + } | ||
| + } ) ); | ||
| + | ||
| + return set; | ||
| + } | ||
| + | ||
| + private Tuple<String, ResolvedLink> importTextDiagram( | ||
| + final String style, | ||
| + final FencedCodeBlock node, | ||
| + final NodeRendererContext context ) { | ||
| + | ||
| + final var type = style.substring( STYLE_DIAGRAM_LEN ); | ||
| + final var content = node.getContentChars().normalizeEOL(); | ||
| + final var text = mInlineEvaluator.apply( content ); | ||
| + final var server = mContext.getImageServer(); | ||
| + final var source = DiagramUrlGenerator.toUrl( server, type, text ); | ||
| + final var link = context.resolveLink( LINK, source, false ); | ||
| + | ||
| + return new Tuple<>( source, link ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Evaluates an R expression. This will take into consideration any | ||
| + * key/value pairs passed in from the document, such as width and height | ||
| + * attributes of the form: <code>{r width=5 height=5}</code>. | ||
| + * | ||
| + * @param node The {@link FencedCodeBlock} to evaluate using R. | ||
| + * @param context Used to resolve the link that refers to any resulting | ||
| + * image produced by the R chunk (such as a plot). | ||
| + * @return The SVG text string associated with the content produced by | ||
| + * the chunk (such as a graphical data plot). | ||
| + */ | ||
| + @SuppressWarnings( "unused" ) | ||
| + private Tuple<String, ResolvedLink> evaluateRChunk( | ||
| + final FencedCodeBlock node, | ||
| + final NodeRendererContext context ) { | ||
| + final var content = node.getContentChars().normalizeEOL().trim(); | ||
| + final var text = mRVariableProcessor.apply( content ); | ||
| + final var hash = Integer.toHexString( text.hashCode() ); | ||
| + final var filename = format( "%s-%s.svg", APP_TITLE_LOWERCASE, hash ); | ||
| + | ||
| + // The URI helps convert backslashes to forward slashes. | ||
| + final var uri = Path.of( TEMPORARY_DIRECTORY, filename ).toUri(); | ||
| + final var svg = uri.getPath(); | ||
| + final var link = context.resolveLink( LINK, svg, false ); | ||
| + final var dimensions = getAttributes( node.getInfo() ); | ||
| + final var r = format( R_SVG_EXPORT, svg, dimensions, text ); | ||
| + | ||
| + mRChunkEvaluator.apply( r ); | ||
| + | ||
| + return new Tuple<>( svg, link ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Splits attributes of the form <code>{r key1=value2 key2=value2}</code> | ||
| + * into a comma-separated string containing only the key/value pairs, | ||
| + * such as <code>key1=value1,key2=value2</code>. | ||
| + * | ||
| + * @param bs The complete line after the fenced block demarcation. | ||
| + * @return A comma-separated string of name/value pairs. | ||
| + */ | ||
| + private String getAttributes( final BasedSequence bs ) { | ||
| + final var result = new StringBuilder(); | ||
| + final var split = bs.splitList( " " ); | ||
| + final var splits = split.size(); | ||
| + | ||
| + for( var i = 1; i < splits; i++ ) { | ||
| + final var based = split.get( i ).toString(); | ||
| + final var attribute = based.replace( '}', ' ' ); | ||
| + | ||
| + // The order of attribute evaluations is in order of performance. | ||
| + if( !attribute.isBlank() && | ||
| + attribute.indexOf( '=' ) > 1 && | ||
| + attribute.matches( ".*\\d.*" ) ) { | ||
| + | ||
| + // The comma will do double-duty for separating individual attributes | ||
| + // as well as being the comma that separates all attributes from the | ||
| + // SVG image file name. | ||
| + result.append( ',' ).append( attribute ); | ||
| + } | ||
| + } | ||
| + | ||
| + return result.toString(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * This method is a stop-gap because blank lines that contain only | ||
| + * whitespace are collapsed into lines without any spaces. Consequently, | ||
| + * the typesetting software does not honour the blank lines, which | ||
| + * then would otherwise discard blank lines entirely. | ||
| + * <p> | ||
| + * Given the following: | ||
| + * | ||
| + * <pre> | ||
| + * if( bool ) { | ||
| + * | ||
| + * | ||
| + * } | ||
| + * </pre> | ||
| + * <p> | ||
| + * The typesetter would otherwise render this incorrectly as: | ||
| + * | ||
| + * <pre> | ||
| + * if( bool ) { | ||
| + * } | ||
| + * </pre> | ||
| + * <p> | ||
| + */ | ||
| + private void render( | ||
| + final FencedCodeBlock node, | ||
| + final NodeRendererContext context, | ||
| + final HtmlWriter html ) { | ||
| + assert node != null; | ||
| + assert context != null; | ||
| + assert html != null; | ||
| + | ||
| + html.line(); | ||
| + html.srcPosWithTrailingEOL( node.getChars() ) | ||
| + .withAttr() | ||
| + .tag( "pre" ) | ||
| + .openPre(); | ||
| + | ||
| + final var options = context.getHtmlOptions(); | ||
| + final var languageClass = lookupLanguageClass( node, options ); | ||
| + | ||
| + if( !languageClass.isBlank() ) { | ||
| + html.attr( "class", languageClass ); | ||
| + } | ||
| + | ||
| + html.srcPosWithEOL( node.getContentChars() ) | ||
| + .withAttr( CODE_CONTENT ) | ||
| + .tag( "code" ); | ||
| + | ||
| + final var lines = node.getContentLines(); | ||
| + | ||
| + for( final var line : lines ) { | ||
| + if( line.isBlank() ) { | ||
| + html.text( " " ); | ||
| + } | ||
| + | ||
| + html.text( line ); | ||
| + } | ||
| + | ||
| + html.tag( "/code" ); | ||
| + html.tag( "/pre" ) | ||
| + .closePre(); | ||
| + html.lineIf( options.htmlBlockCloseTagEol ); | ||
| + } | ||
| + | ||
| + private String lookupLanguageClass( | ||
| + final FencedCodeBlock node, | ||
| + final HtmlRendererOptions options ) { | ||
| + assert node != null; | ||
| + assert options != null; | ||
| + | ||
| + final var info = node.getInfo(); | ||
| + | ||
| + if( info.isNotNull() && !info.isBlank() ) { | ||
| + final var lang = node | ||
| + .getInfoDelimitedByAny( options.languageDelimiterSet ) | ||
| + .unescape(); | ||
| + return options | ||
| + .languageClassMap | ||
| + .getOrDefault( lang, options.languageClassPrefix + lang ); | ||
| + } | ||
| + | ||
| + return options.noLanguageClass; | ||
| + } | ||
| + } | ||
| + | ||
| + private class Factory implements DelegatingNodeRendererFactory { | ||
| + public Factory() { } | ||
| + | ||
| + @NotNull | ||
| + @Override | ||
| + public NodeRenderer apply( @NotNull final DataHolder options ) { | ||
| + return new CustomRenderer(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Return {@code null} to indicate this may delegate to the core renderer. | ||
| + */ | ||
| + @Override | ||
| + public Set<Class<?>> getDelegates() { | ||
| + return null; | ||
| + } | ||
| + } | ||
| +} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors.markdown.extensions.fences; | ||
| - | ||
| -import com.vladsch.flexmark.html.HtmlWriter; | ||
| -import com.vladsch.flexmark.util.html.Attribute; | ||
| - | ||
| -import java.util.ArrayList; | ||
| -import java.util.List; | ||
| - | ||
| -/** | ||
| - * Responsible for helping to generate an opening {@code div} element. | ||
| - */ | ||
| -public final class OpeningDivBlock extends DivBlock { | ||
| - private final List<Attribute> mAttributes = new ArrayList<>(); | ||
| - | ||
| - OpeningDivBlock( final List<Attribute> attributes ) { | ||
| - assert attributes != null; | ||
| - mAttributes.addAll( attributes ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - void write( final HtmlWriter html ) { | ||
| - mAttributes.forEach( html::attr ); | ||
| - html.withAttr().tag( HTML_DIV ); | ||
| - } | ||
| -} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.blocks; | ||
| + | ||
| +import com.vladsch.flexmark.html.HtmlWriter; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| + | ||
| +/** | ||
| + * Responsible for helping to generate a closing {@code div} element. | ||
| + */ | ||
| +public final class ClosingDivBlock extends DivBlock { | ||
| + public ClosingDivBlock( final BasedSequence chars ) { | ||
| + super( chars ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void write( final HtmlWriter html ) { | ||
| + html.closeTag( HTML_DIV ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.blocks; | ||
| + | ||
| +import com.vladsch.flexmark.html.HtmlWriter; | ||
| +import com.vladsch.flexmark.util.ast.Block; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| +import org.jetbrains.annotations.NotNull; | ||
| + | ||
| +public abstract class DivBlock extends Block { | ||
| + static final CharSequence HTML_DIV = "div"; | ||
| + | ||
| + public DivBlock( final BasedSequence chars ) { | ||
| + super( chars ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + @NotNull | ||
| + public BasedSequence[] getSegments() { | ||
| + return EMPTY_SEGMENTS; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Append an opening or closing HTML div element to the given writer. | ||
| + * | ||
| + * @param html Builds the HTML document to be written. | ||
| + */ | ||
| + public abstract void write( HtmlWriter html ); | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.blocks; | ||
| + | ||
| +import com.vladsch.flexmark.html.HtmlWriter; | ||
| +import com.vladsch.flexmark.util.html.Attribute; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| + | ||
| +import java.util.ArrayList; | ||
| +import java.util.List; | ||
| + | ||
| +/** | ||
| + * Responsible for helping to generate an opening {@code div} element. | ||
| + */ | ||
| +public final class OpeningDivBlock extends DivBlock { | ||
| + private final List<Attribute> mAttributes = new ArrayList<>(); | ||
| + | ||
| + public OpeningDivBlock( final BasedSequence chars, | ||
| + final List<Attribute> attrs ) { | ||
| + super( chars ); | ||
| + assert attrs != null; | ||
| + mAttributes.addAll( attrs ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void write( final HtmlWriter html ) { | ||
| + mAttributes.forEach( html::attr ); | ||
| + html.withAttr().tag( HTML_DIV ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.factories; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory; | ||
| +import com.vladsch.flexmark.parser.block.BlockParserFactory; | ||
| +import com.vladsch.flexmark.util.data.DataHolder; | ||
| + | ||
| +/** | ||
| + * Responsible for creating an instance of {@link DivBlockParserFactory}. | ||
| + */ | ||
| +public class CustomDivBlockParserFactory | ||
| + extends MarkdownCustomBlockParserFactory { | ||
| + @Override | ||
| + public BlockParserFactory createBlockParserFactory( | ||
| + final DataHolder options ) { | ||
| + return new DivBlockParserFactory( options ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.factories; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.fences.parsers.ClosingParser; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.parsers.OpeningParser; | ||
| +import com.vladsch.flexmark.parser.block.AbstractBlockParserFactory; | ||
| +import com.vladsch.flexmark.parser.block.BlockStart; | ||
| +import com.vladsch.flexmark.parser.block.MatchedBlockParser; | ||
| +import com.vladsch.flexmark.parser.block.ParserState; | ||
| +import com.vladsch.flexmark.util.data.DataHolder; | ||
| + | ||
| +import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE; | ||
| +import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE_OPENING; | ||
| + | ||
| +/** | ||
| + * Responsible for creating a fenced div parser that is appropriate for the | ||
| + * type of fenced div encountered: opening or closing. | ||
| + */ | ||
| +public class DivBlockParserFactory extends AbstractBlockParserFactory { | ||
| + public DivBlockParserFactory( final DataHolder options ) { | ||
| + super( options ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Try to match an opening or closing fenced div. | ||
| + * | ||
| + * @param state Block parser state. | ||
| + * @param blockParser Last matched open block parser. | ||
| + * @return Wrapper for the opening or closing parser, upon finding :::. | ||
| + */ | ||
| + @Override | ||
| + public BlockStart tryStart( | ||
| + final ParserState state, final MatchedBlockParser blockParser ) { | ||
| + return | ||
| + state.getIndent() == 0 && FENCE.matcher( state.getLine() ).matches() | ||
| + ? parseFence( state ) | ||
| + : BlockStart.none(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * After finding a fenced div, this will further disambiguate an opening | ||
| + * from a closing fence. | ||
| + * | ||
| + * @param state Block parser state, contains line to parse. | ||
| + * @return Wrapper for the opening or closing parser, upon finding :::. | ||
| + */ | ||
| + private BlockStart parseFence( final ParserState state ) { | ||
| + final var line = state.getLine(); | ||
| + final var fence = FENCE_OPENING.matcher( line.trim() ); | ||
| + | ||
| + return BlockStart.of( | ||
| + fence.matches() | ||
| + ? new OpeningParser( fence.group( 1 ) ) | ||
| + : new ClosingParser( line ) | ||
| + ).atIndex( state.getIndex() ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.factories; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.renderers.FencedDivRenderer; | ||
| +import com.vladsch.flexmark.html.renderer.NodeRenderer; | ||
| +import com.vladsch.flexmark.util.data.DataHolder; | ||
| + | ||
| +public class FencedDivNodeRendererFactory extends MarkdownNodeRendererFactory { | ||
| + @Override | ||
| + protected NodeRenderer createNodeRenderer( final DataHolder options ) { | ||
| + return new FencedDivRenderer(); | ||
| + } | ||
| +} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.factories; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.fences.processors.FencedDivParagraphPreProcessor; | ||
| +import com.vladsch.flexmark.parser.block.ParagraphPreProcessor; | ||
| +import com.vladsch.flexmark.parser.block.ParagraphPreProcessorFactory; | ||
| +import com.vladsch.flexmark.parser.block.ParserState; | ||
| +import com.vladsch.flexmark.parser.core.ReferencePreProcessorFactory; | ||
| +import org.jetbrains.annotations.Nullable; | ||
| + | ||
| +import java.util.Set; | ||
| + | ||
| +public class FencedDivPreProcessorFactory | ||
| + implements ParagraphPreProcessorFactory { | ||
| + | ||
| + @Override | ||
| + public ParagraphPreProcessor apply( final ParserState state ) { | ||
| + return new FencedDivParagraphPreProcessor( state.getProperties() ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public @Nullable Set<Class<?>> getBeforeDependents() { | ||
| + return Set.of(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public @Nullable Set<Class<?>> getAfterDependents() { | ||
| + return Set.of( ReferencePreProcessorFactory.class ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public boolean affectsGlobalScope() { | ||
| + return false; | ||
| + } | ||
| + | ||
| +} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.parsers; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock; | ||
| +import com.vladsch.flexmark.util.ast.Block; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| + | ||
| +/** | ||
| + * Responsible for creating an instance of {@link ClosingDivBlock}. | ||
| + */ | ||
| +public class ClosingParser extends DivBlockParser { | ||
| + private final ClosingDivBlock mBlock; | ||
| + | ||
| + public ClosingParser( final BasedSequence line ) { | ||
| + mBlock = new ClosingDivBlock( line ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public Block getBlock() { | ||
| + return mBlock; | ||
| + } | ||
| +} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.parsers; | ||
| + | ||
| +import com.vladsch.flexmark.parser.block.AbstractBlockParser; | ||
| +import com.vladsch.flexmark.parser.block.BlockContinue; | ||
| +import com.vladsch.flexmark.parser.block.ParserState; | ||
| + | ||
| +/** | ||
| + * Abstracts common {@link OpeningParser} and | ||
| + * {@link ClosingParser} methods. | ||
| + */ | ||
| +public abstract class DivBlockParser extends AbstractBlockParser { | ||
| + @Override | ||
| + public BlockContinue tryContinue( final ParserState state ) { | ||
| + return BlockContinue.none(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public void closeBlock( final ParserState state ) {} | ||
| +} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.parsers; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock; | ||
| +import com.vladsch.flexmark.util.ast.Block; | ||
| +import com.vladsch.flexmark.util.html.Attribute; | ||
| +import com.vladsch.flexmark.util.html.AttributeImpl; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| + | ||
| +import java.util.ArrayList; | ||
| +import java.util.regex.Pattern; | ||
| + | ||
| +import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; | ||
| +import static java.util.regex.Pattern.compile; | ||
| + | ||
| +/** | ||
| + * Responsible for creating an instance of {@link OpeningDivBlock}. | ||
| + */ | ||
| +public class OpeningParser extends DivBlockParser { | ||
| + /** | ||
| + * Matches either individual CSS definitions (id/class, {@code <d>}) or | ||
| + * key/value pairs ({@code <k>} and {@link <v>}). The key/value pair | ||
| + * will match optional quotes. | ||
| + */ | ||
| + private static final Pattern ATTR_PAIRS = compile( | ||
| + "\\s*" + | ||
| + "(?<d>[#.][\\p{Alnum}\\-_]+[^\\s=])|" + | ||
| + "((?<k>[\\p{Alnum}\\-_]+)=" + | ||
| + "\"*(?<v>(?<=\")[^\"]+(?=\")|(\\S+))\"*)", | ||
| + UNICODE_CHARACTER_CLASS ); | ||
| + | ||
| + /** | ||
| + * Matches whether extended syntax is being used. | ||
| + */ | ||
| + private static final Pattern ATTR_CSS = compile( "\\{(.+)}" ); | ||
| + | ||
| + private final OpeningDivBlock mBlock; | ||
| + | ||
| + /** | ||
| + * Parses the arguments upon construction. | ||
| + * | ||
| + * @param args Text after :::, excluding leading/trailing whitespace. | ||
| + */ | ||
| + public OpeningParser( final String args ) { | ||
| + final var attrs = new ArrayList<Attribute>(); | ||
| + final var cssMatcher = ATTR_CSS.matcher( args ); | ||
| + | ||
| + if( cssMatcher.matches() ) { | ||
| + // Split the text between braces into tokens and/or key-value pairs. | ||
| + final var pairMatcher = | ||
| + ATTR_PAIRS.matcher( cssMatcher.group( 1 ) ); | ||
| + | ||
| + while( pairMatcher.find() ) { | ||
| + final var cssDef = pairMatcher.group( "d" ); | ||
| + String cssAttrKey = "class"; | ||
| + final String cssAttrVal; | ||
| + | ||
| + // When no regular CSS definition (id or class), use key/value pairs. | ||
| + if( cssDef == null ) { | ||
| + cssAttrKey = STR."data-\{pairMatcher.group( "k" )}"; | ||
| + cssAttrVal = pairMatcher.group( "v" ); | ||
| + } | ||
| + else { | ||
| + // This will strip the "#" and "." off the start of CSS definition. | ||
| + var index = 1; | ||
| + | ||
| + // Default CSS attribute name is "class", switch to "id" for #. | ||
| + if( cssDef.startsWith( "#" ) ) { | ||
| + cssAttrKey = "id"; | ||
| + } | ||
| + else if( !cssDef.startsWith( "." ) ) { | ||
| + index = 0; | ||
| + } | ||
| + | ||
| + cssAttrVal = cssDef.substring( index ); | ||
| + } | ||
| + | ||
| + attrs.add( AttributeImpl.of( cssAttrKey, cssAttrVal ) ); | ||
| + } | ||
| + } | ||
| + else { | ||
| + attrs.add( AttributeImpl.of( "class", args ) ); | ||
| + } | ||
| + | ||
| + final var chars = BasedSequence.of( args ); | ||
| + | ||
| + mBlock = new OpeningDivBlock( chars, attrs ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public Block getBlock() { | ||
| + return mBlock; | ||
| + } | ||
| +} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.processors; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.fences.parsers.ClosingParser; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.parsers.OpeningParser; | ||
| +import com.vladsch.flexmark.ast.Paragraph; | ||
| +import com.vladsch.flexmark.parser.block.ParagraphPreProcessor; | ||
| +import com.vladsch.flexmark.parser.block.ParserState; | ||
| +import com.vladsch.flexmark.util.data.MutableDataHolder; | ||
| +import com.vladsch.flexmark.util.sequence.BasedSequence; | ||
| + | ||
| +import java.util.ArrayList; | ||
| + | ||
| +import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE; | ||
| +import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE_OPENING; | ||
| + | ||
| +public class FencedDivParagraphPreProcessor | ||
| + implements ParagraphPreProcessor { | ||
| + public FencedDivParagraphPreProcessor( final MutableDataHolder unused ) {} | ||
| + | ||
| + @Override | ||
| + public int preProcessBlock( final Paragraph block, | ||
| + final ParserState state ) { | ||
| + final var lines = block.getContentLines(); | ||
| + | ||
| + // Stores lines matching opening or closing fenced div sigil. | ||
| + final var sigilLines = new ArrayList<BasedSequence>(); | ||
| + | ||
| + for( final var line : lines ) { | ||
| + // Seeks a :::+ sigil. | ||
| + final var fence = FENCE.matcher( line ); | ||
| + | ||
| + if( fence.find() ) { | ||
| + // Attributes after the fence are required to detect an open fence. | ||
| + final var attrs = FENCE_OPENING.matcher( line.trim() ); | ||
| + final var match = attrs.matches(); | ||
| + | ||
| + final var parser = match | ||
| + ? new OpeningParser( attrs.group( 1 ) ) | ||
| + : new ClosingParser( line ); | ||
| + final var divBlock = parser.getBlock(); | ||
| + | ||
| + if( match ) { | ||
| + block.insertBefore( divBlock ); | ||
| + } | ||
| + else { | ||
| + block.insertAfter( divBlock ); | ||
| + } | ||
| + | ||
| + state.blockAdded( divBlock ); | ||
| + | ||
| + // Schedule the line for removal (because it has been handled). | ||
| + sigilLines.add( line ); | ||
| + } | ||
| + } | ||
| + | ||
| + sigilLines.forEach( lines::remove ); | ||
| + | ||
| + return 0; | ||
| + } | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences.renderers; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.blocks.DivBlock; | ||
| +import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock; | ||
| +import com.vladsch.flexmark.html.HtmlWriter; | ||
| +import com.vladsch.flexmark.html.renderer.NodeRenderer; | ||
| +import com.vladsch.flexmark.html.renderer.NodeRendererContext; | ||
| +import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | ||
| +import org.jetbrains.annotations.Nullable; | ||
| + | ||
| +import java.util.Set; | ||
| + | ||
| +/** | ||
| + * Responsible for rendering opening and closing fenced div blocks as HTML | ||
| + * {@code div} elements. | ||
| + */ | ||
| +public class FencedDivRenderer implements NodeRenderer { | ||
| + @Nullable | ||
| + @Override | ||
| + public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | ||
| + return Set.of( | ||
| + new NodeRenderingHandler<>( OpeningDivBlock.class, this::render ), | ||
| + new NodeRenderingHandler<>( ClosingDivBlock.class, this::render ) | ||
| + ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Renders the fenced div block as an HTML {@code <div></div>} element. | ||
| + */ | ||
| + void render( | ||
| + final DivBlock node, | ||
| + final NodeRendererContext context, | ||
| + final HtmlWriter html ) { | ||
| + node.write( html ); | ||
| + } | ||
| +} | ||
| +package com.keenwrite.processors.markdown.extensions; | ||
| + | ||
| +import com.vladsch.flexmark.html.HtmlRenderer; | ||
| +import com.vladsch.flexmark.parser.Parser; | ||
| +import com.vladsch.flexmark.parser.Parser.ParserExtension; | ||
| +import org.junit.jupiter.api.TestInstance; | ||
| +import org.junit.jupiter.params.ParameterizedTest; | ||
| +import org.junit.jupiter.params.provider.Arguments; | ||
| +import org.junit.jupiter.params.provider.MethodSource; | ||
| + | ||
| +import java.util.LinkedList; | ||
| +import java.util.List; | ||
| +import java.util.stream.Stream; | ||
| + | ||
| +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| + | ||
| +@TestInstance( TestInstance.Lifecycle.PER_CLASS ) | ||
| +public abstract class ExtensionTest { | ||
| + private final List<ParserExtension> mExtensions = new LinkedList<>(); | ||
| + | ||
| + @ParameterizedTest | ||
| + @MethodSource( "getDocuments" ) | ||
| + public void test_Extensions_Markdown_Html( | ||
| + final String input, final String expected | ||
| + ) { | ||
| + final var pBuilder = Parser.builder(); | ||
| + final var hBuilder = HtmlRenderer.builder(); | ||
| + final var parser = pBuilder.extensions( mExtensions ).build(); | ||
| + final var renderer = hBuilder.extensions( mExtensions ).build(); | ||
| + | ||
| + final var document = parser.parse( input ); | ||
| + final var actual = renderer.render( document ); | ||
| + | ||
| + assertEquals( expected, actual ); | ||
| + } | ||
| + | ||
| + protected void addExtension( final ParserExtension extension ) { | ||
| + mExtensions.add( extension ); | ||
| + } | ||
| + | ||
| + protected Arguments args( final String in, final String out ) { | ||
| + return Arguments.of( in, out ); | ||
| + } | ||
| + | ||
| + protected abstract Stream<Arguments> getDocuments(); | ||
| +} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.markdown.extensions.fences; | ||
| + | ||
| +import com.keenwrite.processors.markdown.extensions.ExtensionTest; | ||
| +import org.junit.jupiter.api.BeforeAll; | ||
| +import org.junit.jupiter.params.provider.Arguments; | ||
| + | ||
| +import java.util.stream.Stream; | ||
| + | ||
| +public class FencedDivExtensionTest extends ExtensionTest { | ||
| + | ||
| + @BeforeAll | ||
| + protected void setup() { | ||
| + addExtension( FencedDivExtension.create() ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected Stream<Arguments> getDocuments() { | ||
| + return Stream.of( | ||
| + args( | ||
| + """ | ||
| + > | ||
| + > ::: {.concurrent title="3:58"} | ||
| + > Line 1 | ||
| + > ::: | ||
| + > | ||
| + > ::: {.concurrent title="3:59"} | ||
| + > Line 2 | ||
| + > ::: | ||
| + > | ||
| + > ::: {.concurrent title="4:00"} | ||
| + > Line 3 | ||
| + > ::: | ||
| + > | ||
| + """, | ||
| + """ | ||
| + <blockquote> | ||
| + <div class="concurrent" data-title="3:58"> | ||
| + <p>Line 1</p> | ||
| + </div><div class="concurrent" data-title="3:59"> | ||
| + <p>Line 2</p> | ||
| + </div><div class="concurrent" data-title="4:00"> | ||
| + <p>Line 3</p> | ||
| + </div> | ||
| + </blockquote> | ||
| + """ | ||
| + ), | ||
| + args( | ||
| + """ | ||
| + > Hello | ||
| + > | ||
| + > ::: world | ||
| + > Adventures | ||
| + > | ||
| + > in **absolute** | ||
| + > | ||
| + > nesting. | ||
| + > ::: | ||
| + > | ||
| + > Goodbye | ||
| + """, | ||
| + """ | ||
| + <blockquote> | ||
| + <p>Hello</p> | ||
| + <div class="world"> | ||
| + <p>Adventures</p> | ||
| + <p>in <strong>absolute</strong></p> | ||
| + <p>nesting.</p> | ||
| + </div> | ||
| + <p>Goodbye</p> | ||
| + </blockquote> | ||
| + """ | ||
| + ) | ||
| + ); | ||
| + } | ||
| +} | ||
| package com.keenwrite.processors.markdown.extensions.references; | ||
| +import com.keenwrite.processors.markdown.extensions.ExtensionTest; | ||
| import com.keenwrite.processors.markdown.extensions.captions.CaptionExtension; | ||
| import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension; | ||
| import com.keenwrite.processors.markdown.extensions.tex.TexExtension; | ||
| import com.vladsch.flexmark.ext.definition.DefinitionExtension; | ||
| import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension; | ||
| import com.vladsch.flexmark.ext.superscript.SuperscriptExtension; | ||
| import com.vladsch.flexmark.ext.tables.TablesExtension; | ||
| -import com.vladsch.flexmark.html.HtmlRenderer; | ||
| -import com.vladsch.flexmark.parser.Parser; | ||
| -import com.vladsch.flexmark.parser.Parser.ParserExtension; | ||
| -import org.junit.jupiter.params.ParameterizedTest; | ||
| +import org.junit.jupiter.api.BeforeAll; | ||
| import org.junit.jupiter.params.provider.Arguments; | ||
| -import org.junit.jupiter.params.provider.MethodSource; | ||
| -import java.util.LinkedList; | ||
| -import java.util.List; | ||
| import java.util.stream.Stream; | ||
| import static com.keenwrite.ExportFormat.XHTML_TEX; | ||
| import static com.keenwrite.processors.ProcessorContext.Mutator; | ||
| import static com.keenwrite.processors.ProcessorContext.builder; | ||
| -import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| @SuppressWarnings( "SpellCheckingInspection" ) | ||
| -public class CaptionsAndCrossReferencesExtensionTest { | ||
| - @ParameterizedTest | ||
| - @MethodSource( "testDocuments" ) | ||
| - public void test_References_Documents_Html( | ||
| - final String input, final String expected | ||
| - ) { | ||
| - final var pBuilder = Parser.builder(); | ||
| - final var hBuilder = HtmlRenderer.builder(); | ||
| - final var extensions = createExtensions(); | ||
| - final var parser = pBuilder.extensions( extensions ).build(); | ||
| - final var renderer = hBuilder.extensions( extensions ).build(); | ||
| - | ||
| - final var document = parser.parse( input ); | ||
| - final var actual = renderer.render( document ); | ||
| - | ||
| - assertEquals( expected, actual ); | ||
| - } | ||
| - | ||
| - private static Stream<Arguments> testDocuments() { | ||
| +public class CaptionsAndCrossReferencesExtensionTest extends ExtensionTest { | ||
| + protected Stream<Arguments> getDocuments() { | ||
| return Stream.of( | ||
| args( | ||
| 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? | ||
| """, | ||
| """ | ||
| $$E=mc^2$$ | ||
| - | ||
| + | ||
| :: Caption {#eqn:energy} | ||
| """, | ||
| main :: IO () | ||
| ``` | ||
| - | ||
| + | ||
| :: Source code caption {#listing:haskell1} | ||
| """, | ||
| ::: warning | ||
| Do not eat processed **sugar**. | ||
| - | ||
| + | ||
| Seriously. | ||
| ::: | ||
| - | ||
| + | ||
| :: Caption {#warning:sugar} | ||
| """, | ||
| """ | ||
|  | ||
| - | ||
| + | ||
| :: Caption {#fig:label} | ||
| """, | ||
| """ | ||
|  | ||
| - | ||
| + | ||
| :: Caption **bold** {#fig:label} *italics* | ||
| """, | ||
| > | ||
| > I've traded my halo for horns and a whip. | ||
| - | ||
| + | ||
| :: Meschiya Lake - Lucky Devil {#lyrics:blues} | ||
| """, | ||
| | 1 | 2 | 3 | | ||
| | 4 | 5 | 6 | | ||
| - | ||
| + | ||
| :: Caption {#tbl:label} | ||
| """, | ||
| @enduml | ||
| ``` | ||
| - | ||
| + | ||
| :: Diagram {#dia:seq1} | ||
| """, | ||
| Gas on down to future town, | ||
| Make prophecy take hold. | ||
| - | ||
| + | ||
| Warnin' sign, cent'ry old: | ||
| When buyin' coal, air is sold. | ||
| ) | ||
| ); | ||
| - } | ||
| - | ||
| - private static Arguments args( final String in, final String out ) { | ||
| - return Arguments.of( in, out ); | ||
| } | ||
| - private List<ParserExtension> createExtensions() { | ||
| - final var extensions = new LinkedList<ParserExtension>(); | ||
| + @BeforeAll | ||
| + protected void setup() { | ||
| final var context = builder() | ||
| .with( Mutator::setExportFormat, XHTML_TEX ) | ||
| .build(); | ||
| - | ||
| - extensions.add( TexExtension.create( s -> s, context ) ); | ||
| - extensions.add( DefinitionExtension.create() ); | ||
| - extensions.add( StrikethroughSubscriptExtension.create() ); | ||
| - extensions.add( SuperscriptExtension.create() ); | ||
| - extensions.add( TablesExtension.create() ); | ||
| - extensions.add( FencedDivExtension.create() ); | ||
| - extensions.add( CrossReferenceExtension.create() ); | ||
| - extensions.add( CaptionExtension.create() ); | ||
| - return extensions; | ||
| + addExtension( TexExtension.create( s -> s, context ) ); | ||
| + addExtension( DefinitionExtension.create() ); | ||
| + addExtension( StrikethroughSubscriptExtension.create() ); | ||
| + addExtension( SuperscriptExtension.create() ); | ||
| + addExtension( TablesExtension.create() ); | ||
| + addExtension( FencedDivExtension.create() ); | ||
| + addExtension( CrossReferenceExtension.create() ); | ||
| + addExtension( CaptionExtension.create() ); | ||
| } | ||
| } | ||
| Author | DaveJarvis <email> |
|---|---|
| Date | 2024-08-23 16:26:32 GMT-0700 |
| Commit | 251e920a3275f2ab3ef865dcbe33b5426393ccda |
| Parent | 026363a |
| Delta | 978 lines added, 692 lines removed, 286-line increase |