Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
src/main/java/com/keenwrite/processors/html/XhtmlProcessor.java
*/
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 );
src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
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 ) ) {
src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionPostProcessor.java
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;
src/main/java/com/keenwrite/processors/markdown/extensions/fences/ClosingDivBlock.java
-/* 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 );
- }
-}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/DivBlock.java
-/* 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 );
-}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedBlockExtension.java
-/* 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;
- }
- }
-}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivExtension.java
-/* 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>
- * &lt;div id="verse" class="p d" data-k="v" data-author="Emily Dickson"&gt;
+ * 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;
- }
}
}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivNodeRendererFactory.java
-/* 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();
- }
-}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivRenderer.java
-/* 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 );
- }
-}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/ImageBlockExtension.java
+/* 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;
+ }
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/OpeningDivBlock.java
-/* 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 );
- }
-}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/ClosingDivBlock.java
+/* 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 );
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/DivBlock.java
+/* 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 );
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/OpeningDivBlock.java
+/* 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 );
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/CustomDivBlockParserFactory.java
+/* 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 );
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/DivBlockParserFactory.java
+/* 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() );
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/FencedDivNodeRendererFactory.java
+/* 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();
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/FencedDivPreProcessorFactory.java
+/* 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;
+ }
+
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/ClosingParser.java
+/* 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;
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/DivBlockParser.java
+/* 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 ) {}
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/OpeningParser.java
+/* 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;
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/processors/FencedDivParagraphPreProcessor.java
+/* 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;
+ }
+}
src/main/java/com/keenwrite/processors/markdown/extensions/fences/renderers/FencedDivRenderer.java
+/* 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 );
+ }
+}
src/test/java/com/keenwrite/processors/markdown/extensions/ExtensionTest.java
+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();
+}
src/test/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivExtensionTest.java
+/* 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>
+ """
+ )
+ );
+ }
+}
src/test/java/com/keenwrite/processors/markdown/extensions/references/CaptionsAndCrossReferencesExtensionTest.java
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}
""",
"""
![alt text](tunnel)
-
+
:: Caption {#fig:label}
""",
"""
![kitteh](kitten)
-
+
:: 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() );
}
}

Allows nesting fences within blockquotes

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