Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Embed diagrams via kroki

AuthorDaveJarvis <email>
Date2020-12-29 22:49:25 GMT-0800
Commit80ef259fbd077234030a27a7d2fee0edad8700c0
Parent28bba97
Delta183 lines added, 22 lines removed, 161-line increase
src/main/java/com/keenwrite/ui/actions/ApplicationMenuBar.java
get( "Main.menu.help" ),
addAction( "help.about", e -> actions.help‿about() )
- )
+ ) );
//@formatter:on
- );
//@formatter:off
getAction( "insert.unordered_list" ),
getAction( "insert.ordered_list" )
- //@formatter:on
);
+ //@formatter:on
return new VBox( menuBar, toolBar );
src/main/java/com/keenwrite/processors/markdown/FencedBlockExtension.java
+/* Copyright 2020 White Magic Software, Ltd. -- All rights reserved. */
+package com.keenwrite.processors.markdown;
+
+import com.keenwrite.processors.DefinitionProcessor;
+import com.keenwrite.processors.IdentityProcessor;
+import com.keenwrite.processors.ProcessorContext;
+import com.vladsch.flexmark.ast.FencedCodeBlock;
+import com.vladsch.flexmark.html.renderer.DelegatingNodeRendererFactory;
+import com.vladsch.flexmark.html.renderer.NodeRenderer;
+import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
+import com.vladsch.flexmark.util.data.DataHolder;
+import com.vladsch.flexmark.util.data.MutableDataHolder;
+import com.vladsch.flexmark.util.sequence.BasedSequence;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.zip.Deflater;
+
+import static com.keenwrite.StatusBarNotifier.clue;
+import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
+import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
+import static com.vladsch.flexmark.html.renderer.LinkType.LINK;
+import static java.lang.String.format;
+import static java.util.Base64.getUrlEncoder;
+import static java.util.zip.Deflater.BEST_COMPRESSION;
+import static java.util.zip.Deflater.FULL_FLUSH;
+
+/**
+ * Responsible for converting textual diagram descriptions into HTML image
+ * elements.
+ */
+public class FencedBlockExtension implements HtmlRendererExtension {
+ private final static String DIAGRAM_STYLE = "diagram-";
+ private final static int DIAGRAM_STYLE_LEN = DIAGRAM_STYLE.length();
+
+ private final DefinitionProcessor mProcessor;
+
+ public FencedBlockExtension( final ProcessorContext context ) {
+ assert context != null;
+ mProcessor = new DefinitionProcessor( IdentityProcessor.INSTANCE, 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 DefinitionProcessor} to substitute
+ * variable definitions. This is necessary because the order of processors
+ * matters. If the {@link DefinitionProcessor} 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 context Used to create a new {@link DefinitionProcessor}.
+ * @return A new {@link FencedBlockExtension} capable of shunting ASCII
+ * diagrams to a service for conversion to SVG.
+ */
+ public static FencedBlockExtension create( final ProcessorContext context ) {
+ return new FencedBlockExtension( context );
+ }
+
+ @Override
+ public void rendererOptions( @NotNull final MutableDataHolder options ) {
+ }
+
+ @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();
+ }
+
+ 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() );
+
+ if( style.startsWith( DIAGRAM_STYLE ) ) {
+ final var type = style.substring( DIAGRAM_STYLE_LEN );
+ final var content = node.getContentChars().normalizeEOL();
+ final var text = FencedBlockExtension.this.mProcessor.apply( content );
+ final var encoded = encode( text );
+ final var source = format(
+ "https://kroki.io/%s/svg/%s", type, encoded );
+
+ final var link = context.resolveLink( LINK, source, false );
+
+ html.attr( "src", source );
+ html.withAttr( link );
+ html.tagVoid( "img" );
+ }
+ else {
+ context.delegateRender();
+ }
+ } ) );
+
+ return set;
+ }
+
+ private byte[] compress( byte[] source ) {
+ final var inLen = source.length;
+ final var result = new byte[ inLen ];
+ final var deflater = new Deflater( BEST_COMPRESSION );
+
+ deflater.setInput( source, 0, inLen );
+ deflater.finish();
+ final var outLen = deflater.deflate( result, 0, inLen, FULL_FLUSH );
+ deflater.end();
+
+ try( final var out = new ByteArrayOutputStream() ) {
+ out.write( result, 0, outLen );
+ return out.toByteArray();
+ } catch( final Exception ex ) {
+ clue( ex );
+ throw new RuntimeException( ex );
+ }
+ }
+
+ private String encode( final String decoded ) {
+ return getUrlEncoder().encodeToString( compress( decoded.getBytes() ) );
+ }
+ }
+
+ 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/ImageLinkExtension.java
}
+ private final Path mBasePath;
+ private final Workspace mWorkspace;
+
+ private ImageLinkExtension(
+ @NotNull final Path basePath, @NotNull final Workspace workspace ) {
+ mBasePath = basePath;
+ mWorkspace = workspace;
+ }
+
+ @Override
+ public void rendererOptions( @NotNull final MutableDataHolder options ) {
+ }
+
+ @Override
+ public void extend(
+ @NotNull final Builder builder, @NotNull final String rendererType ) {
+ builder.linkResolverFactory( new Factory() );
+ }
+
private class Factory extends IndependentLinkResolverFactory {
@Override
return mBasePath;
}
- }
-
- private final Path mBasePath;
- private final Workspace mWorkspace;
-
- private ImageLinkExtension(
- @NotNull final Path basePath, @NotNull final Workspace workspace ) {
- mBasePath = basePath;
- mWorkspace = workspace;
- }
-
- @Override
- public void rendererOptions( @NotNull final MutableDataHolder options ) {
- }
-
- @Override
- public void extend(
- @NotNull final Builder builder, @NotNull final String rendererType ) {
- builder.linkResolverFactory( new Factory() );
}
}
src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
final var extensions = createExtensions( path, format, workspace );
+ extensions.add( FencedBlockExtension.create( context ) );
extensions.add( CaretExtension.create( context.getCaret() ) );