Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M README.md
3737
Using Java, first follow these one-time setup steps:
3838
39
1. Download the *Full version* of the Java Runtime Environment, [JRE 21](https://bell-sw.com/pages/downloads).
39
1. Download the *Full version* of the Java Runtime Environment, [JRE 22](https://bell-sw.com/pages/downloads).
4040
   * JavaFX, which is bundled with BellSoft's *Full version*, is required.
4141
1. Install the JRE (include JRE's `bin` directory in the `PATH` environment variable).
M libs/keenquotes.jar
Binary file
M src/main/java/com/keenwrite/commands/ConcatenateCommand.java
55
package com.keenwrite.commands;
66
7
import com.keenwrite.io.SysFile;
78
import com.keenwrite.util.AlphanumComparator;
89
import com.keenwrite.util.RangeValidator;
...
4647
    mExtension = extension;
4748
    mRange = range;
49
  }
50
51
  public static int toDigits( final String filename, final int fallback ) {
52
    final var stripped = filename.replaceAll( "\\D", "" );
53
54
    return stripped.isEmpty() ? fallback : Integer.parseInt( stripped );
4855
  }
4956
5057
  public String call() throws IOException {
51
    final var glob = "**/*." + mExtension;
58
    final var glob = STR."**/*.\{mExtension}";
5259
    final var files = new ArrayList<Path>();
5360
    final var text = new StringBuilder( DOCUMENT_LENGTH );
5461
    final var chapter = new AtomicInteger();
5562
    final var eol = lineSeparator();
56
5763
    final var validator = new RangeValidator( mRange );
5864
5965
    walk( mParent, glob, files::add );
6066
    files.sort( new AlphanumComparator<>() );
6167
    files.forEach( file -> {
6268
      try {
63
        if( validator.test( chapter.incrementAndGet() ) ) {
69
        final var filename = SysFile.getFileName( file );
70
        final var digits = toDigits( filename, chapter.incrementAndGet() );
71
72
        if( validator.test( digits ) ) {
6473
          clue( "Main.status.export.concat", file );
6574
M src/main/java/com/keenwrite/processors/html/XhtmlProcessor.java
4040
 */
4141
public final class XhtmlProcessor extends ExecutorProcessor<String> {
42
  private static final Curler sTypographer =
42
  private static final Curler sCurler =
4343
    new Curler( createContractions(), FILTER_XML, true );
4444
...
102102
      final var curl = mContext.getCurlQuotes();
103103
104
      return curl ? sTypographer.apply( document ) : document;
104
      return curl ? sCurler.apply( document ) : document;
105105
    } catch( final Exception ex ) {
106106
      clue( ex );
M src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
1111
import com.keenwrite.processors.variable.VariableProcessor;
1212
import com.keenwrite.processors.markdown.extensions.caret.CaretExtension;
13
import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension;
13
import com.keenwrite.processors.markdown.extensions.fences.ImageBlockExtension;
1414
import com.keenwrite.processors.markdown.extensions.images.ImageLinkExtension;
1515
import com.keenwrite.processors.markdown.extensions.outline.DocumentOutlineExtension;
...
8484
    result.add( ImageLinkExtension.create( context ) );
8585
    result.add( TexExtension.create( evaluator, context ) );
86
    result.add( FencedBlockExtension.create( processor, evaluator, context ) );
86
    result.add( ImageBlockExtension.create( processor, evaluator, context ) );
8787
8888
    if( context.isExportFormat( ExportFormat.NONE ) ) {
M src/main/java/com/keenwrite/processors/markdown/extensions/captions/CaptionPostProcessor.java
55
package com.keenwrite.processors.markdown.extensions.captions;
66
7
import com.keenwrite.processors.markdown.extensions.fences.ClosingDivBlock;
8
import com.keenwrite.processors.markdown.extensions.fences.OpeningDivBlock;
7
import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock;
8
import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock;
99
import com.vladsch.flexmark.parser.block.NodePostProcessor;
1010
import com.vladsch.flexmark.util.ast.Node;
D src/main/java/com/keenwrite/processors/markdown/extensions/fences/ClosingDivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
6
/**
7
 * Responsible for helping to generate a closing {@code div} element.
8
 */
9
public final class ClosingDivBlock extends DivBlock {
10
  @Override
11
  void write( final HtmlWriter html ) {
12
    html.closeTag( HTML_DIV );
13
  }
14
}
151
D src/main/java/com/keenwrite/processors/markdown/extensions/fences/DivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.ast.Block;
6
import com.vladsch.flexmark.util.sequence.BasedSequence;
7
import org.jetbrains.annotations.NotNull;
8
9
abstract class DivBlock extends Block {
10
  static final CharSequence HTML_DIV = "div";
11
12
  @Override
13
  @NotNull
14
  public BasedSequence[] getSegments() {
15
    return EMPTY_SEGMENTS;
16
  }
17
18
  /**
19
   * Append an opening or closing HTML div element to the given writer.
20
   *
21
   * @param html Builds the HTML document to be written.
22
   */
23
  abstract void write( HtmlWriter html );
24
}
251
D src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedBlockExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.keenwrite.preview.DiagramUrlGenerator;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.processors.variable.VariableProcessor;
8
import com.keenwrite.processors.markdown.MarkdownProcessor;
9
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
10
import com.keenwrite.processors.r.RChunkEvaluator;
11
import com.keenwrite.processors.variable.RVariableProcessor;
12
import com.vladsch.flexmark.ast.FencedCodeBlock;
13
import com.vladsch.flexmark.html.HtmlRendererOptions;
14
import com.vladsch.flexmark.html.HtmlWriter;
15
import com.vladsch.flexmark.html.renderer.*;
16
import com.vladsch.flexmark.util.data.DataHolder;
17
import com.vladsch.flexmark.util.sequence.BasedSequence;
18
import com.whitemagicsoftware.keenquotes.util.Tuple;
19
import org.jetbrains.annotations.NotNull;
20
21
import java.nio.file.Path;
22
import java.util.HashSet;
23
import java.util.Set;
24
import java.util.function.Function;
25
26
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
27
import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY;
28
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
29
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
30
import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
31
import static com.vladsch.flexmark.html.renderer.LinkType.LINK;
32
import static java.lang.String.format;
33
34
/**
35
 * Responsible for converting textual diagram descriptions into HTML image
36
 * elements.
37
 */
38
public final class FencedBlockExtension extends HtmlRendererAdapter {
39
  /**
40
   * Ensure that the device is always closed to prevent an out-of-resources
41
   * error, regardless of whether the R expression the user tries to evaluate
42
   * is valid by swallowing errors alongside a {@code finally} block.
43
   */
44
  private static final String R_SVG_EXPORT =
45
    "tryCatch({svg('%s'%s)%n%s%n},finally={dev.off()})%n";
46
47
  private static final String STYLE_DIAGRAM = "diagram-";
48
  private static final int STYLE_DIAGRAM_LEN = STYLE_DIAGRAM.length();
49
50
  private static final String STYLE_R_CHUNK = "{r";
51
52
  private static final class VerbatimRVariableProcessor
53
    extends RVariableProcessor {
54
55
    public VerbatimRVariableProcessor(
56
      final Processor<String> successor, final ProcessorContext context ) {
57
      super( successor, context );
58
    }
59
60
    @Override
61
    protected String processValue( final String value ) {
62
      return value;
63
    }
64
  }
65
66
  private final RChunkEvaluator mRChunkEvaluator;
67
  private final Function<String, String> mInlineEvaluator;
68
69
  private final Processor<String> mRVariableProcessor;
70
  private final ProcessorContext mContext;
71
72
  public FencedBlockExtension(
73
    final Processor<String> processor,
74
    final Function<String, String> evaluator,
75
    final ProcessorContext context ) {
76
    assert processor != null;
77
    assert context != null;
78
    mContext = context;
79
    mRChunkEvaluator = new RChunkEvaluator();
80
    mInlineEvaluator = evaluator;
81
    mRVariableProcessor = new VerbatimRVariableProcessor( IDENTITY, context );
82
  }
83
84
  /**
85
   * Creates a new parser for fenced blocks. This calls out to a web service
86
   * to generate SVG files of text diagrams.
87
   * <p>
88
   * Internally, this creates a {@link VariableProcessor} to substitute
89
   * variable definitions. This is necessary because the order of processors
90
   * matters. If the {@link VariableProcessor} comes before an instance of
91
   * {@link MarkdownProcessor}, for example, then the caret position in the
92
   * preview pane will not align with the caret position in the editor
93
   * pane. The {@link MarkdownProcessor} must come before all else. However,
94
   * when parsing fenced blocks, the variables within the block must be
95
   * interpolated before being sent to the diagram web service.
96
   * </p>
97
   *
98
   * @param processor Used to pre-process the text.
99
   * @return A new {@link FencedBlockExtension} capable of shunting ASCII
100
   * diagrams to a service for conversion to SVG.
101
   */
102
  public static FencedBlockExtension create(
103
    final Processor<String> processor,
104
    final Function<String, String> evaluator,
105
    final ProcessorContext context ) {
106
    assert processor != null;
107
    assert context != null;
108
    return new FencedBlockExtension( processor, evaluator, context );
109
  }
110
111
  @Override
112
  public void extend(
113
    @NotNull final Builder builder, @NotNull final String rendererType ) {
114
    builder.nodeRendererFactory( new Factory() );
115
  }
116
117
  /**
118
   * Converts the given {@link BasedSequence} to a lowercase value.
119
   *
120
   * @param text The character string to convert to lowercase.
121
   * @return The lowercase text value, or the empty string for no text.
122
   */
123
  private static String sanitize( final BasedSequence text ) {
124
    assert text != null;
125
    return text.toString().toLowerCase();
126
  }
127
128
  /**
129
   * Responsible for generating images from a fenced block that contains a
130
   * diagram reference.
131
   */
132
  private class CustomRenderer implements NodeRenderer {
133
134
    @Override
135
    public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
136
      final var set = new HashSet<NodeRenderingHandler<?>>();
137
138
      set.add( new NodeRenderingHandler<>(
139
        FencedCodeBlock.class, ( node, context, html ) -> {
140
        final var style = sanitize( node.getInfo() );
141
        final Tuple<String, ResolvedLink> imagePair;
142
143
        if( style.startsWith( STYLE_DIAGRAM ) ) {
144
          imagePair = importTextDiagram( style, node, context );
145
146
          html.attr( "src", imagePair.item1() );
147
          html.withAttr( imagePair.item2() );
148
          html.tagVoid( "img" );
149
        }
150
        else if( style.startsWith( STYLE_R_CHUNK ) ) {
151
          imagePair = evaluateRChunk( node, context );
152
153
          html.attr( "src", imagePair.item1() );
154
          html.withAttr( imagePair.item2() );
155
          html.tagVoid( "img" );
156
        }
157
        else {
158
          // TODO: Revert to using context.delegateRender() after flexmark
159
          //   is updated to no longer trim blank lines up to the EOL.
160
          render( node, context, html );
161
        }
162
      } ) );
163
164
      return set;
165
    }
166
167
    private Tuple<String, ResolvedLink> importTextDiagram(
168
      final String style,
169
      final FencedCodeBlock node,
170
      final NodeRendererContext context ) {
171
172
      final var type = style.substring( STYLE_DIAGRAM_LEN );
173
      final var content = node.getContentChars().normalizeEOL();
174
      final var text = mInlineEvaluator.apply( content );
175
      final var server = mContext.getImageServer();
176
      final var source = DiagramUrlGenerator.toUrl( server, type, text );
177
      final var link = context.resolveLink( LINK, source, false );
178
179
      return new Tuple<>( source, link );
180
    }
181
182
    /**
183
     * Evaluates an R expression. This will take into consideration any
184
     * key/value pairs passed in from the document, such as width and height
185
     * attributes of the form: <code>{r width=5 height=5}</code>.
186
     *
187
     * @param node    The {@link FencedCodeBlock} to evaluate using R.
188
     * @param context Used to resolve the link that refers to any resulting
189
     *                image produced by the R chunk (such as a plot).
190
     * @return The SVG text string associated with the content produced by
191
     * the chunk (such as a graphical data plot).
192
     */
193
    @SuppressWarnings( "unused" )
194
    private Tuple<String, ResolvedLink> evaluateRChunk(
195
      final FencedCodeBlock node,
196
      final NodeRendererContext context ) {
197
      final var content = node.getContentChars().normalizeEOL().trim();
198
      final var text = mRVariableProcessor.apply( content );
199
      final var hash = Integer.toHexString( text.hashCode() );
200
      final var filename = format( "%s-%s.svg", APP_TITLE_LOWERCASE, hash );
201
202
      // The URI helps convert backslashes to forward slashes.
203
      final var uri = Path.of( TEMPORARY_DIRECTORY, filename ).toUri();
204
      final var svg = uri.getPath();
205
      final var link = context.resolveLink( LINK, svg, false );
206
      final var dimensions = getAttributes( node.getInfo() );
207
      final var r = format( R_SVG_EXPORT, svg, dimensions, text );
208
209
      mRChunkEvaluator.apply( r );
210
211
      return new Tuple<>( svg, link );
212
    }
213
214
    /**
215
     * Splits attributes of the form <code>{r key1=value2 key2=value2}</code>
216
     * into a comma-separated string containing only the key/value pairs,
217
     * such as <code>key1=value1,key2=value2</code>.
218
     *
219
     * @param bs The complete line after the fenced block demarcation.
220
     * @return A comma-separated string of name/value pairs.
221
     */
222
    private String getAttributes( final BasedSequence bs ) {
223
      final var result = new StringBuilder();
224
      final var split = bs.splitList( " " );
225
      final var splits = split.size();
226
227
      for( var i = 1; i < splits; i++ ) {
228
        final var based = split.get( i ).toString();
229
        final var attribute = based.replace( '}', ' ' );
230
231
        // The order of attribute evaluations is in order of performance.
232
        if( !attribute.isBlank() &&
233
          attribute.indexOf( '=' ) > 1 &&
234
          attribute.matches( ".*\\d.*" ) ) {
235
236
          // The comma will do double-duty for separating individual attributes
237
          // as well as being the comma that separates all attributes from the
238
          // SVG image file name.
239
          result.append( ',' ).append( attribute );
240
        }
241
      }
242
243
      return result.toString();
244
    }
245
246
    /**
247
     * This method is a stop-gap because blank lines that contain only
248
     * whitespace are collapsed into lines without any spaces. Consequently,
249
     * the typesetting software does not honour the blank lines, which
250
     * then would otherwise discard blank lines entirely.
251
     * <p>
252
     * Given the following:
253
     *
254
     * <pre>
255
     *   if( bool ) {
256
     *
257
     *
258
     *   }
259
     * </pre>
260
     * <p>
261
     * The typesetter would otherwise render this incorrectly as:
262
     *
263
     * <pre>
264
     *   if( bool ) {
265
     *   }
266
     * </pre>
267
     * <p>
268
     */
269
    private void render(
270
      final FencedCodeBlock node,
271
      final NodeRendererContext context,
272
      final HtmlWriter html ) {
273
      assert node != null;
274
      assert context != null;
275
      assert html != null;
276
277
      html.line();
278
      html.srcPosWithTrailingEOL( node.getChars() )
279
          .withAttr()
280
          .tag( "pre" )
281
          .openPre();
282
283
      final var options = context.getHtmlOptions();
284
      final var languageClass = lookupLanguageClass( node, options );
285
286
      if( !languageClass.isBlank() ) {
287
        html.attr( "class", languageClass );
288
      }
289
290
      html.srcPosWithEOL( node.getContentChars() )
291
          .withAttr( CODE_CONTENT )
292
          .tag( "code" );
293
294
      final var lines = node.getContentLines();
295
296
      for( final var line : lines ) {
297
        if( line.isBlank() ) {
298
          html.text( "    " );
299
        }
300
301
        html.text( line );
302
      }
303
304
      html.tag( "/code" );
305
      html.tag( "/pre" )
306
          .closePre();
307
      html.lineIf( options.htmlBlockCloseTagEol );
308
    }
309
310
    private String lookupLanguageClass(
311
      final FencedCodeBlock node,
312
      final HtmlRendererOptions options ) {
313
      assert node != null;
314
      assert options != null;
315
316
      final var info = node.getInfo();
317
318
      if( info.isNotNull() && !info.isBlank() ) {
319
        final var lang = node
320
          .getInfoDelimitedByAny( options.languageDelimiterSet )
321
          .unescape();
322
        return options
323
          .languageClassMap
324
          .getOrDefault( lang, options.languageClassPrefix + lang );
325
      }
326
327
      return options.noLanguageClass;
328
    }
329
  }
330
331
  private class Factory implements DelegatingNodeRendererFactory {
332
    public Factory() { }
333
334
    @NotNull
335
    @Override
336
    public NodeRenderer apply( @NotNull final DataHolder options ) {
337
      return new CustomRenderer();
338
    }
339
340
    /**
341
     * Return {@code null} to indicate this may delegate to the core renderer.
342
     */
343
    @Override
344
    public Set<Class<?>> getDelegates() {
345
      return null;
346
    }
347
  }
348
}
3491
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
1
/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. */
22
package com.keenwrite.processors.markdown.extensions.fences;
33
4
import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory;
4
import com.keenwrite.processors.markdown.extensions.common.MarkdownParserExtension;
55
import com.keenwrite.processors.markdown.extensions.common.MarkdownRendererExtension;
6
import com.keenwrite.processors.markdown.extensions.fences.factories.CustomDivBlockParserFactory;
7
import com.keenwrite.processors.markdown.extensions.fences.factories.FencedDivNodeRendererFactory;
8
import com.keenwrite.processors.markdown.extensions.fences.factories.FencedDivPreProcessorFactory;
69
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
710
import com.vladsch.flexmark.parser.Parser.Builder;
8
import com.vladsch.flexmark.parser.block.*;
9
import com.vladsch.flexmark.util.ast.Block;
10
import com.vladsch.flexmark.util.data.DataHolder;
11
import com.vladsch.flexmark.util.html.Attribute;
12
import com.vladsch.flexmark.util.html.AttributeImpl;
1311
14
import java.util.ArrayList;
1512
import java.util.regex.Pattern;
1613
...
3633
 * </p>
3734
 * <p>
38
 * ::: {#verse .p .d k=v author="Emily Dickinson"}
35
 * ::: {#verse .p .d k=v author="Dickinson"}
3936
 * Because I could not stop for Death --
4037
 * He kindly stopped for me --
...
4744
 * The second example produces the following starting {@code div} element:
4845
 * </p>
46
 * {@literal <div id="verse" class="p d" data-k="v" data-author="Dickson">}
47
 *
4948
 * <p>
50
 * &lt;div id="verse" class="p d" data-k="v" data-author="Emily Dickson"&gt;
49
 * This will parse fenced divs embedded inside of blockquote environments.
5150
 * </p>
5251
 */
53
public class FencedDivExtension extends MarkdownRendererExtension {
52
public class FencedDivExtension extends MarkdownRendererExtension
53
  implements MarkdownParserExtension {
5454
  /**
5555
   * Matches any number of colons at start of line. This will match both the
5656
   * opening and closing fences, with any number of colons.
5757
   */
58
  private static final Pattern FENCE = compile( "^:::.*" );
58
  public static final Pattern FENCE = compile( "^:::+.*" );
5959
6060
  /**
6161
   * After a fenced div is detected, this will match the opening fence.
62
   */
63
  private static final Pattern FENCE_OPENING = compile(
64
    "^:::+\\s+([\\p{Alnum}-_]+|\\{.+})\\s*$",
65
    UNICODE_CHARACTER_CLASS );
66
67
  /**
68
   * Matches whether extended syntax is being used.
69
   */
70
  private static final Pattern ATTR_CSS = compile( "\\{(.+)}" );
71
72
  /**
73
   * Matches either individual CSS definitions (id/class, {@code <d>}) or
74
   * key/value pairs ({@code <k>} and {@link <v>}). The key/value pair
75
   * will match optional quotes.
7662
   */
77
  private static final Pattern ATTR_PAIRS = compile(
78
    "\\s*" +
79
    "(?<d>[#.][\\p{Alnum}-_]+[^\\s=])|" +
80
    "((?<k>[\\p{Alnum}-_]+)=" +
81
    "\"*(?<v>(?<=\")[^\"]+(?=\")|(\\S+))\"*)",
63
  public static final Pattern FENCE_OPENING = compile(
64
    "^:::+\\s+([\\p{Alnum}\\-_]+|\\{.+})\\s*$",
8265
    UNICODE_CHARACTER_CLASS );
8366
8467
  public static FencedDivExtension create() {
8568
    return new FencedDivExtension();
8669
  }
8770
8871
  @Override
8972
  public void extend( final Builder builder ) {
90
    builder.customBlockParserFactory( new DivBlockParserFactory() );
73
    builder.customBlockParserFactory( new CustomDivBlockParserFactory() );
74
    builder.paragraphPreProcessorFactory( new FencedDivPreProcessorFactory() );
9175
  }
9276
9377
  @Override
9478
  protected NodeRendererFactory createNodeRendererFactory() {
9579
    return new FencedDivNodeRendererFactory();
96
  }
97
98
  /**
99
   * Responsible for creating an instance of {@link ParserFactory}.
100
   */
101
  private static class DivBlockParserFactory
102
    extends MarkdownCustomBlockParserFactory {
103
    @Override
104
    public BlockParserFactory createBlockParserFactory( final DataHolder options ) {
105
      return new ParserFactory( options );
106
    }
107
  }
108
109
  /**
110
   * Responsible for creating a fenced div parser that is appropriate for the
111
   * type of fenced div encountered: opening or closing.
112
   */
113
  private static class ParserFactory extends AbstractBlockParserFactory {
114
    public ParserFactory( final DataHolder options ) {
115
      super( options );
116
    }
117
118
    /**
119
     * Try to match an opening or closing fenced div.
120
     *
121
     * @param state              Block parser state.
122
     * @param matchedBlockParser Last matched open block parser.
123
     * @return Wrapper for the opening or closing parser, upon finding :::.
124
     */
125
    @Override
126
    public BlockStart tryStart(
127
      final ParserState state, final MatchedBlockParser matchedBlockParser ) {
128
      return
129
        state.getIndent() == 0 && FENCE.matcher( state.getLine() ).matches()
130
          ? parseFence( state )
131
          : BlockStart.none();
132
    }
133
134
    /**
135
     * After finding a fenced div, this will further disambiguate an opening
136
     * from a closing fence.
137
     *
138
     * @param state Block parser state, contains line to parse.
139
     * @return Wrapper for the opening or closing parser, upon finding :::.
140
     */
141
    private BlockStart parseFence( final ParserState state ) {
142
      final var fence = FENCE_OPENING.matcher( state.getLine() );
143
144
      return BlockStart.of(
145
        fence.matches()
146
          ? new OpeningParser( fence.group( 1 ) )
147
          : new ClosingParser()
148
      ).atIndex( state.getIndex() );
149
    }
150
  }
151
152
  /**
153
   * Abstracts common {@link OpeningParser} and {@link ClosingParser} methods.
154
   */
155
  private static abstract class DivBlockParser extends AbstractBlockParser {
156
    @Override
157
    public BlockContinue tryContinue( final ParserState state ) {
158
      return BlockContinue.none();
159
    }
160
161
    @Override
162
    public void closeBlock( final ParserState state ) {}
163
  }
164
165
  /**
166
   * Responsible for creating an instance of {@link OpeningDivBlock}.
167
   */
168
  private static class OpeningParser extends DivBlockParser {
169
    private final OpeningDivBlock mBlock;
170
171
    /**
172
     * Parses the arguments upon construction.
173
     *
174
     * @param args Text after :::, excluding leading/trailing whitespace.
175
     */
176
    public OpeningParser( final String args ) {
177
      final var attrs = new ArrayList<Attribute>();
178
      final var cssMatcher = ATTR_CSS.matcher( args );
179
180
      if( cssMatcher.matches() ) {
181
        // Split the text between braces into tokens and/or key-value pairs.
182
        final var pairMatcher = ATTR_PAIRS.matcher( cssMatcher.group( 1 ) );
183
184
        while( pairMatcher.find() ) {
185
          final var cssDef = pairMatcher.group( "d" );
186
          String cssAttrKey = "class";
187
          String cssAttrVal;
188
189
          // When no regular CSS definition (id or class), use key/value pairs.
190
          if( cssDef == null ) {
191
            cssAttrKey = "data-" + pairMatcher.group( "k" );
192
            cssAttrVal = pairMatcher.group( "v" );
193
          }
194
          else {
195
            // This will strip the "#" and "." off the start of CSS definition.
196
            var index = 1;
197
198
            // Default CSS attribute name is "class", switch to "id" for #.
199
            if( cssDef.startsWith( "#" ) ) {
200
              cssAttrKey = "id";
201
            }
202
            else if( !cssDef.startsWith( "." ) ) {
203
              index = 0;
204
            }
205
206
            cssAttrVal = cssDef.substring( index );
207
          }
208
209
          attrs.add( AttributeImpl.of( cssAttrKey, cssAttrVal ) );
210
        }
211
      }
212
      else {
213
        attrs.add( AttributeImpl.of( "class", args ) );
214
      }
215
216
      mBlock = new OpeningDivBlock( attrs );
217
    }
218
219
    @Override
220
    public Block getBlock() {
221
      return mBlock;
222
    }
223
  }
224
225
  /**
226
   * Responsible for creating an instance of {@link ClosingDivBlock}.
227
   */
228
  private static class ClosingParser extends DivBlockParser {
229
    private final ClosingDivBlock mBlock = new ClosingDivBlock();
230
231
    @Override
232
    public Block getBlock() {
233
      return mBlock;
234
    }
23580
  }
23681
}
D src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivNodeRendererFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory;
5
import com.vladsch.flexmark.html.renderer.NodeRenderer;
6
import com.vladsch.flexmark.util.data.DataHolder;
7
8
class FencedDivNodeRendererFactory extends MarkdownNodeRendererFactory {
9
  @Override
10
  protected NodeRenderer createNodeRenderer( final DataHolder options ) {
11
    return new FencedDivRenderer();
12
  }
13
}
141
D src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivRenderer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.html.renderer.NodeRenderer;
6
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
7
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
8
import org.jetbrains.annotations.Nullable;
9
10
import java.util.Set;
11
12
/**
13
 * Responsible for rendering opening and closing fenced div blocks as HTML
14
 * {@code div} elements.
15
 */
16
class FencedDivRenderer implements NodeRenderer {
17
  @Nullable
18
  @Override
19
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
20
    return Set.of(
21
      new NodeRenderingHandler<>( OpeningDivBlock.class, this::render ),
22
      new NodeRenderingHandler<>( ClosingDivBlock.class, this::render )
23
    );
24
  }
25
26
  /**
27
   * Renders the fenced div block as an HTML {@code <div></div>} element.
28
   */
29
  void render(
30
    final DivBlock node,
31
    final NodeRendererContext context,
32
    final HtmlWriter html ) {
33
    node.write( html );
34
  }
35
}
361
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/ImageBlockExtension.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.keenwrite.preview.DiagramUrlGenerator;
5
import com.keenwrite.processors.Processor;
6
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.processors.variable.VariableProcessor;
8
import com.keenwrite.processors.markdown.MarkdownProcessor;
9
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
10
import com.keenwrite.processors.r.RChunkEvaluator;
11
import com.keenwrite.processors.variable.RVariableProcessor;
12
import com.vladsch.flexmark.ast.FencedCodeBlock;
13
import com.vladsch.flexmark.html.HtmlRendererOptions;
14
import com.vladsch.flexmark.html.HtmlWriter;
15
import com.vladsch.flexmark.html.renderer.*;
16
import com.vladsch.flexmark.util.data.DataHolder;
17
import com.vladsch.flexmark.util.sequence.BasedSequence;
18
import com.whitemagicsoftware.keenquotes.util.Tuple;
19
import org.jetbrains.annotations.NotNull;
20
21
import java.nio.file.Path;
22
import java.util.HashSet;
23
import java.util.Set;
24
import java.util.function.Function;
25
26
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
27
import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY;
28
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
29
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
30
import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
31
import static com.vladsch.flexmark.html.renderer.LinkType.LINK;
32
import static java.lang.String.format;
33
34
/**
35
 * Responsible for converting textual diagram descriptions into HTML image
36
 * elements.
37
 */
38
public final class ImageBlockExtension extends HtmlRendererAdapter {
39
  /**
40
   * Ensure that the device is always closed to prevent an out-of-resources
41
   * error, regardless of whether the R expression the user tries to evaluate
42
   * is valid by swallowing errors alongside a {@code finally} block.
43
   */
44
  private static final String R_SVG_EXPORT =
45
    "tryCatch({svg('%s'%s)%n%s%n},finally={dev.off()})%n";
46
47
  private static final String STYLE_DIAGRAM = "diagram-";
48
  private static final int STYLE_DIAGRAM_LEN = STYLE_DIAGRAM.length();
49
50
  private static final String STYLE_R_CHUNK = "{r";
51
52
  private static final class VerbatimRVariableProcessor
53
    extends RVariableProcessor {
54
55
    public VerbatimRVariableProcessor(
56
      final Processor<String> successor, final ProcessorContext context ) {
57
      super( successor, context );
58
    }
59
60
    @Override
61
    protected String processValue( final String value ) {
62
      return value;
63
    }
64
  }
65
66
  private final RChunkEvaluator mRChunkEvaluator;
67
  private final Function<String, String> mInlineEvaluator;
68
69
  private final Processor<String> mRVariableProcessor;
70
  private final ProcessorContext mContext;
71
72
  public ImageBlockExtension(
73
    final Processor<String> processor,
74
    final Function<String, String> evaluator,
75
    final ProcessorContext context ) {
76
    assert processor != null;
77
    assert context != null;
78
    mContext = context;
79
    mRChunkEvaluator = new RChunkEvaluator();
80
    mInlineEvaluator = evaluator;
81
    mRVariableProcessor = new VerbatimRVariableProcessor( IDENTITY, context );
82
  }
83
84
  /**
85
   * Creates a new parser for fenced blocks. This calls out to a web service
86
   * to generate SVG files of text diagrams.
87
   * <p>
88
   * Internally, this creates a {@link VariableProcessor} to substitute
89
   * variable definitions. This is necessary because the order of processors
90
   * matters. If the {@link VariableProcessor} comes before an instance of
91
   * {@link MarkdownProcessor}, for example, then the caret position in the
92
   * preview pane will not align with the caret position in the editor
93
   * pane. The {@link MarkdownProcessor} must come before all else. However,
94
   * when parsing fenced blocks, the variables within the block must be
95
   * interpolated before being sent to the diagram web service.
96
   * </p>
97
   *
98
   * @param processor Used to pre-process the text.
99
   * @return A new {@link ImageBlockExtension} capable of shunting ASCII
100
   * diagrams to a service for conversion to SVG.
101
   */
102
  public static ImageBlockExtension create(
103
    final Processor<String> processor,
104
    final Function<String, String> evaluator,
105
    final ProcessorContext context ) {
106
    assert processor != null;
107
    assert context != null;
108
    return new ImageBlockExtension( processor, evaluator, context );
109
  }
110
111
  @Override
112
  public void extend(
113
    @NotNull final Builder builder, @NotNull final String rendererType ) {
114
    builder.nodeRendererFactory( new Factory() );
115
  }
116
117
  /**
118
   * Converts the given {@link BasedSequence} to a lowercase value.
119
   *
120
   * @param text The character string to convert to lowercase.
121
   * @return The lowercase text value, or the empty string for no text.
122
   */
123
  private static String sanitize( final BasedSequence text ) {
124
    assert text != null;
125
    return text.toString().toLowerCase();
126
  }
127
128
  /**
129
   * Responsible for generating images from a fenced block that contains a
130
   * diagram reference.
131
   */
132
  private class CustomRenderer implements NodeRenderer {
133
134
    @Override
135
    public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
136
      final var set = new HashSet<NodeRenderingHandler<?>>();
137
138
      set.add( new NodeRenderingHandler<>(
139
        FencedCodeBlock.class, ( node, context, html ) -> {
140
        final var style = sanitize( node.getInfo() );
141
        final Tuple<String, ResolvedLink> imagePair;
142
143
        if( style.startsWith( STYLE_DIAGRAM ) ) {
144
          imagePair = importTextDiagram( style, node, context );
145
146
          html.attr( "src", imagePair.item1() );
147
          html.withAttr( imagePair.item2() );
148
          html.tagVoid( "img" );
149
        }
150
        else if( style.startsWith( STYLE_R_CHUNK ) ) {
151
          imagePair = evaluateRChunk( node, context );
152
153
          html.attr( "src", imagePair.item1() );
154
          html.withAttr( imagePair.item2() );
155
          html.tagVoid( "img" );
156
        }
157
        else {
158
          // TODO: Revert to using context.delegateRender() after flexmark
159
          //   is updated to no longer trim blank lines up to the EOL.
160
          render( node, context, html );
161
        }
162
      } ) );
163
164
      return set;
165
    }
166
167
    private Tuple<String, ResolvedLink> importTextDiagram(
168
      final String style,
169
      final FencedCodeBlock node,
170
      final NodeRendererContext context ) {
171
172
      final var type = style.substring( STYLE_DIAGRAM_LEN );
173
      final var content = node.getContentChars().normalizeEOL();
174
      final var text = mInlineEvaluator.apply( content );
175
      final var server = mContext.getImageServer();
176
      final var source = DiagramUrlGenerator.toUrl( server, type, text );
177
      final var link = context.resolveLink( LINK, source, false );
178
179
      return new Tuple<>( source, link );
180
    }
181
182
    /**
183
     * Evaluates an R expression. This will take into consideration any
184
     * key/value pairs passed in from the document, such as width and height
185
     * attributes of the form: <code>{r width=5 height=5}</code>.
186
     *
187
     * @param node    The {@link FencedCodeBlock} to evaluate using R.
188
     * @param context Used to resolve the link that refers to any resulting
189
     *                image produced by the R chunk (such as a plot).
190
     * @return The SVG text string associated with the content produced by
191
     * the chunk (such as a graphical data plot).
192
     */
193
    @SuppressWarnings( "unused" )
194
    private Tuple<String, ResolvedLink> evaluateRChunk(
195
      final FencedCodeBlock node,
196
      final NodeRendererContext context ) {
197
      final var content = node.getContentChars().normalizeEOL().trim();
198
      final var text = mRVariableProcessor.apply( content );
199
      final var hash = Integer.toHexString( text.hashCode() );
200
      final var filename = format( "%s-%s.svg", APP_TITLE_LOWERCASE, hash );
201
202
      // The URI helps convert backslashes to forward slashes.
203
      final var uri = Path.of( TEMPORARY_DIRECTORY, filename ).toUri();
204
      final var svg = uri.getPath();
205
      final var link = context.resolveLink( LINK, svg, false );
206
      final var dimensions = getAttributes( node.getInfo() );
207
      final var r = format( R_SVG_EXPORT, svg, dimensions, text );
208
209
      mRChunkEvaluator.apply( r );
210
211
      return new Tuple<>( svg, link );
212
    }
213
214
    /**
215
     * Splits attributes of the form <code>{r key1=value2 key2=value2}</code>
216
     * into a comma-separated string containing only the key/value pairs,
217
     * such as <code>key1=value1,key2=value2</code>.
218
     *
219
     * @param bs The complete line after the fenced block demarcation.
220
     * @return A comma-separated string of name/value pairs.
221
     */
222
    private String getAttributes( final BasedSequence bs ) {
223
      final var result = new StringBuilder();
224
      final var split = bs.splitList( " " );
225
      final var splits = split.size();
226
227
      for( var i = 1; i < splits; i++ ) {
228
        final var based = split.get( i ).toString();
229
        final var attribute = based.replace( '}', ' ' );
230
231
        // The order of attribute evaluations is in order of performance.
232
        if( !attribute.isBlank() &&
233
          attribute.indexOf( '=' ) > 1 &&
234
          attribute.matches( ".*\\d.*" ) ) {
235
236
          // The comma will do double-duty for separating individual attributes
237
          // as well as being the comma that separates all attributes from the
238
          // SVG image file name.
239
          result.append( ',' ).append( attribute );
240
        }
241
      }
242
243
      return result.toString();
244
    }
245
246
    /**
247
     * This method is a stop-gap because blank lines that contain only
248
     * whitespace are collapsed into lines without any spaces. Consequently,
249
     * the typesetting software does not honour the blank lines, which
250
     * then would otherwise discard blank lines entirely.
251
     * <p>
252
     * Given the following:
253
     *
254
     * <pre>
255
     *   if( bool ) {
256
     *
257
     *
258
     *   }
259
     * </pre>
260
     * <p>
261
     * The typesetter would otherwise render this incorrectly as:
262
     *
263
     * <pre>
264
     *   if( bool ) {
265
     *   }
266
     * </pre>
267
     * <p>
268
     */
269
    private void render(
270
      final FencedCodeBlock node,
271
      final NodeRendererContext context,
272
      final HtmlWriter html ) {
273
      assert node != null;
274
      assert context != null;
275
      assert html != null;
276
277
      html.line();
278
      html.srcPosWithTrailingEOL( node.getChars() )
279
          .withAttr()
280
          .tag( "pre" )
281
          .openPre();
282
283
      final var options = context.getHtmlOptions();
284
      final var languageClass = lookupLanguageClass( node, options );
285
286
      if( !languageClass.isBlank() ) {
287
        html.attr( "class", languageClass );
288
      }
289
290
      html.srcPosWithEOL( node.getContentChars() )
291
          .withAttr( CODE_CONTENT )
292
          .tag( "code" );
293
294
      final var lines = node.getContentLines();
295
296
      for( final var line : lines ) {
297
        if( line.isBlank() ) {
298
          html.text( "    " );
299
        }
300
301
        html.text( line );
302
      }
303
304
      html.tag( "/code" );
305
      html.tag( "/pre" )
306
          .closePre();
307
      html.lineIf( options.htmlBlockCloseTagEol );
308
    }
309
310
    private String lookupLanguageClass(
311
      final FencedCodeBlock node,
312
      final HtmlRendererOptions options ) {
313
      assert node != null;
314
      assert options != null;
315
316
      final var info = node.getInfo();
317
318
      if( info.isNotNull() && !info.isBlank() ) {
319
        final var lang = node
320
          .getInfoDelimitedByAny( options.languageDelimiterSet )
321
          .unescape();
322
        return options
323
          .languageClassMap
324
          .getOrDefault( lang, options.languageClassPrefix + lang );
325
      }
326
327
      return options.noLanguageClass;
328
    }
329
  }
330
331
  private class Factory implements DelegatingNodeRendererFactory {
332
    public Factory() { }
333
334
    @NotNull
335
    @Override
336
    public NodeRenderer apply( @NotNull final DataHolder options ) {
337
      return new CustomRenderer();
338
    }
339
340
    /**
341
     * Return {@code null} to indicate this may delegate to the core renderer.
342
     */
343
    @Override
344
    public Set<Class<?>> getDelegates() {
345
      return null;
346
    }
347
  }
348
}
1349
D src/main/java/com/keenwrite/processors/markdown/extensions/fences/OpeningDivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.html.Attribute;
6
7
import java.util.ArrayList;
8
import java.util.List;
9
10
/**
11
 * Responsible for helping to generate an opening {@code div} element.
12
 */
13
public final class OpeningDivBlock extends DivBlock {
14
  private final List<Attribute> mAttributes = new ArrayList<>();
15
16
  OpeningDivBlock( final List<Attribute> attributes ) {
17
    assert attributes != null;
18
    mAttributes.addAll( attributes );
19
  }
20
21
  @Override
22
  void write( final HtmlWriter html ) {
23
    mAttributes.forEach( html::attr );
24
    html.withAttr().tag( HTML_DIV );
25
  }
26
}
271
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/ClosingDivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.blocks;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.sequence.BasedSequence;
6
7
/**
8
 * Responsible for helping to generate a closing {@code div} element.
9
 */
10
public final class ClosingDivBlock extends DivBlock {
11
  public ClosingDivBlock( final BasedSequence chars ) {
12
    super( chars );
13
  }
14
15
  @Override
16
  public void write( final HtmlWriter html ) {
17
    html.closeTag( HTML_DIV );
18
  }
19
}
120
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/DivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.blocks;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.ast.Block;
6
import com.vladsch.flexmark.util.sequence.BasedSequence;
7
import org.jetbrains.annotations.NotNull;
8
9
public abstract class DivBlock extends Block {
10
  static final CharSequence HTML_DIV = "div";
11
12
  public DivBlock( final BasedSequence chars ) {
13
    super( chars );
14
  }
15
16
  @Override
17
  @NotNull
18
  public BasedSequence[] getSegments() {
19
    return EMPTY_SEGMENTS;
20
  }
21
22
  /**
23
   * Append an opening or closing HTML div element to the given writer.
24
   *
25
   * @param html Builds the HTML document to be written.
26
   */
27
  public abstract void write( HtmlWriter html );
28
}
129
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/blocks/OpeningDivBlock.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.blocks;
3
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.util.html.Attribute;
6
import com.vladsch.flexmark.util.sequence.BasedSequence;
7
8
import java.util.ArrayList;
9
import java.util.List;
10
11
/**
12
 * Responsible for helping to generate an opening {@code div} element.
13
 */
14
public final class OpeningDivBlock extends DivBlock {
15
  private final List<Attribute> mAttributes = new ArrayList<>();
16
17
  public OpeningDivBlock( final BasedSequence chars,
18
                          final List<Attribute> attrs ) {
19
    super( chars );
20
    assert attrs != null;
21
    mAttributes.addAll( attrs );
22
  }
23
24
  @Override
25
  public void write( final HtmlWriter html ) {
26
    mAttributes.forEach( html::attr );
27
    html.withAttr().tag( HTML_DIV );
28
  }
29
}
130
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/CustomDivBlockParserFactory.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.factories;
6
7
import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory;
8
import com.vladsch.flexmark.parser.block.BlockParserFactory;
9
import com.vladsch.flexmark.util.data.DataHolder;
10
11
/**
12
 * Responsible for creating an instance of {@link DivBlockParserFactory}.
13
 */
14
public class CustomDivBlockParserFactory
15
  extends MarkdownCustomBlockParserFactory {
16
  @Override
17
  public BlockParserFactory createBlockParserFactory(
18
    final DataHolder options ) {
19
    return new DivBlockParserFactory( options );
20
  }
21
}
122
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/DivBlockParserFactory.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.factories;
6
7
import com.keenwrite.processors.markdown.extensions.fences.parsers.ClosingParser;
8
import com.keenwrite.processors.markdown.extensions.fences.parsers.OpeningParser;
9
import com.vladsch.flexmark.parser.block.AbstractBlockParserFactory;
10
import com.vladsch.flexmark.parser.block.BlockStart;
11
import com.vladsch.flexmark.parser.block.MatchedBlockParser;
12
import com.vladsch.flexmark.parser.block.ParserState;
13
import com.vladsch.flexmark.util.data.DataHolder;
14
15
import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE;
16
import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE_OPENING;
17
18
/**
19
 * Responsible for creating a fenced div parser that is appropriate for the
20
 * type of fenced div encountered: opening or closing.
21
 */
22
public class DivBlockParserFactory extends AbstractBlockParserFactory {
23
  public DivBlockParserFactory( final DataHolder options ) {
24
    super( options );
25
  }
26
27
  /**
28
   * Try to match an opening or closing fenced div.
29
   *
30
   * @param state       Block parser state.
31
   * @param blockParser Last matched open block parser.
32
   * @return Wrapper for the opening or closing parser, upon finding :::.
33
   */
34
  @Override
35
  public BlockStart tryStart(
36
    final ParserState state, final MatchedBlockParser blockParser ) {
37
    return
38
      state.getIndent() == 0 && FENCE.matcher( state.getLine() ).matches()
39
        ? parseFence( state )
40
        : BlockStart.none();
41
  }
42
43
  /**
44
   * After finding a fenced div, this will further disambiguate an opening
45
   * from a closing fence.
46
   *
47
   * @param state Block parser state, contains line to parse.
48
   * @return Wrapper for the opening or closing parser, upon finding :::.
49
   */
50
  private BlockStart parseFence( final ParserState state ) {
51
    final var line = state.getLine();
52
    final var fence = FENCE_OPENING.matcher( line.trim() );
53
54
    return BlockStart.of(
55
      fence.matches()
56
        ? new OpeningParser( fence.group( 1 ) )
57
        : new ClosingParser( line )
58
    ).atIndex( state.getIndex() );
59
  }
60
}
161
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/FencedDivNodeRendererFactory.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.factories;
3
4
import com.keenwrite.processors.markdown.extensions.common.MarkdownNodeRendererFactory;
5
import com.keenwrite.processors.markdown.extensions.fences.renderers.FencedDivRenderer;
6
import com.vladsch.flexmark.html.renderer.NodeRenderer;
7
import com.vladsch.flexmark.util.data.DataHolder;
8
9
public class FencedDivNodeRendererFactory extends MarkdownNodeRendererFactory {
10
  @Override
11
  protected NodeRenderer createNodeRenderer( final DataHolder options ) {
12
    return new FencedDivRenderer();
13
  }
14
}
115
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/factories/FencedDivPreProcessorFactory.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.factories;
6
7
import com.keenwrite.processors.markdown.extensions.fences.processors.FencedDivParagraphPreProcessor;
8
import com.vladsch.flexmark.parser.block.ParagraphPreProcessor;
9
import com.vladsch.flexmark.parser.block.ParagraphPreProcessorFactory;
10
import com.vladsch.flexmark.parser.block.ParserState;
11
import com.vladsch.flexmark.parser.core.ReferencePreProcessorFactory;
12
import org.jetbrains.annotations.Nullable;
13
14
import java.util.Set;
15
16
public class FencedDivPreProcessorFactory
17
  implements ParagraphPreProcessorFactory {
18
19
  @Override
20
  public ParagraphPreProcessor apply( final ParserState state ) {
21
    return new FencedDivParagraphPreProcessor( state.getProperties() );
22
  }
23
24
  @Override
25
  public @Nullable Set<Class<?>> getBeforeDependents() {
26
    return Set.of();
27
  }
28
29
  @Override
30
  public @Nullable Set<Class<?>> getAfterDependents() {
31
    return Set.of( ReferencePreProcessorFactory.class );
32
  }
33
34
  @Override
35
  public boolean affectsGlobalScope() {
36
    return false;
37
  }
38
39
}
140
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/ClosingParser.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.parsers;
6
7
import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock;
8
import com.vladsch.flexmark.util.ast.Block;
9
import com.vladsch.flexmark.util.sequence.BasedSequence;
10
11
/**
12
 * Responsible for creating an instance of {@link ClosingDivBlock}.
13
 */
14
public class ClosingParser extends DivBlockParser {
15
  private final ClosingDivBlock mBlock;
16
17
  public ClosingParser( final BasedSequence line ) {
18
    mBlock = new ClosingDivBlock( line );
19
  }
20
21
  @Override
22
  public Block getBlock() {
23
    return mBlock;
24
  }
25
}
126
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/DivBlockParser.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.parsers;
6
7
import com.vladsch.flexmark.parser.block.AbstractBlockParser;
8
import com.vladsch.flexmark.parser.block.BlockContinue;
9
import com.vladsch.flexmark.parser.block.ParserState;
10
11
/**
12
 * Abstracts common {@link OpeningParser} and
13
 * {@link ClosingParser} methods.
14
 */
15
public abstract class DivBlockParser extends AbstractBlockParser {
16
  @Override
17
  public BlockContinue tryContinue( final ParserState state ) {
18
    return BlockContinue.none();
19
  }
20
21
  @Override
22
  public void closeBlock( final ParserState state ) {}
23
}
124
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/parsers/OpeningParser.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.parsers;
6
7
import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock;
8
import com.vladsch.flexmark.util.ast.Block;
9
import com.vladsch.flexmark.util.html.Attribute;
10
import com.vladsch.flexmark.util.html.AttributeImpl;
11
import com.vladsch.flexmark.util.sequence.BasedSequence;
12
13
import java.util.ArrayList;
14
import java.util.regex.Pattern;
15
16
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
17
import static java.util.regex.Pattern.compile;
18
19
/**
20
 * Responsible for creating an instance of {@link OpeningDivBlock}.
21
 */
22
public class OpeningParser extends DivBlockParser {
23
  /**
24
   * Matches either individual CSS definitions (id/class, {@code <d>}) or
25
   * key/value pairs ({@code <k>} and {@link <v>}). The key/value pair
26
   * will match optional quotes.
27
   */
28
  private static final Pattern ATTR_PAIRS = compile(
29
    "\\s*" +
30
    "(?<d>[#.][\\p{Alnum}\\-_]+[^\\s=])|" +
31
    "((?<k>[\\p{Alnum}\\-_]+)=" +
32
    "\"*(?<v>(?<=\")[^\"]+(?=\")|(\\S+))\"*)",
33
    UNICODE_CHARACTER_CLASS );
34
35
  /**
36
   * Matches whether extended syntax is being used.
37
   */
38
  private static final Pattern ATTR_CSS = compile( "\\{(.+)}" );
39
40
  private final OpeningDivBlock mBlock;
41
42
  /**
43
   * Parses the arguments upon construction.
44
   *
45
   * @param args Text after :::, excluding leading/trailing whitespace.
46
   */
47
  public OpeningParser( final String args ) {
48
    final var attrs = new ArrayList<Attribute>();
49
    final var cssMatcher = ATTR_CSS.matcher( args );
50
51
    if( cssMatcher.matches() ) {
52
      // Split the text between braces into tokens and/or key-value pairs.
53
      final var pairMatcher =
54
        ATTR_PAIRS.matcher( cssMatcher.group( 1 ) );
55
56
      while( pairMatcher.find() ) {
57
        final var cssDef = pairMatcher.group( "d" );
58
        String cssAttrKey = "class";
59
        final String cssAttrVal;
60
61
        // When no regular CSS definition (id or class), use key/value pairs.
62
        if( cssDef == null ) {
63
          cssAttrKey = STR."data-\{pairMatcher.group( "k" )}";
64
          cssAttrVal = pairMatcher.group( "v" );
65
        }
66
        else {
67
          // This will strip the "#" and "." off the start of CSS definition.
68
          var index = 1;
69
70
          // Default CSS attribute name is "class", switch to "id" for #.
71
          if( cssDef.startsWith( "#" ) ) {
72
            cssAttrKey = "id";
73
          }
74
          else if( !cssDef.startsWith( "." ) ) {
75
            index = 0;
76
          }
77
78
          cssAttrVal = cssDef.substring( index );
79
        }
80
81
        attrs.add( AttributeImpl.of( cssAttrKey, cssAttrVal ) );
82
      }
83
    }
84
    else {
85
      attrs.add( AttributeImpl.of( "class", args ) );
86
    }
87
88
    final var chars = BasedSequence.of( args );
89
90
    mBlock = new OpeningDivBlock( chars, attrs );
91
  }
92
93
  @Override
94
  public Block getBlock() {
95
    return mBlock;
96
  }
97
}
198
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/processors/FencedDivParagraphPreProcessor.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences.processors;
6
7
import com.keenwrite.processors.markdown.extensions.fences.parsers.ClosingParser;
8
import com.keenwrite.processors.markdown.extensions.fences.parsers.OpeningParser;
9
import com.vladsch.flexmark.ast.Paragraph;
10
import com.vladsch.flexmark.parser.block.ParagraphPreProcessor;
11
import com.vladsch.flexmark.parser.block.ParserState;
12
import com.vladsch.flexmark.util.data.MutableDataHolder;
13
import com.vladsch.flexmark.util.sequence.BasedSequence;
14
15
import java.util.ArrayList;
16
17
import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE;
18
import static com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension.FENCE_OPENING;
19
20
public class FencedDivParagraphPreProcessor
21
  implements ParagraphPreProcessor {
22
  public FencedDivParagraphPreProcessor( final MutableDataHolder unused ) {}
23
24
  @Override
25
  public int preProcessBlock( final Paragraph block,
26
                              final ParserState state ) {
27
    final var lines = block.getContentLines();
28
29
    // Stores lines matching opening or closing fenced div sigil.
30
    final var sigilLines = new ArrayList<BasedSequence>();
31
32
    for( final var line : lines ) {
33
      // Seeks a :::+ sigil.
34
      final var fence = FENCE.matcher( line );
35
36
      if( fence.find() ) {
37
        // Attributes after the fence are required to detect an open fence.
38
        final var attrs = FENCE_OPENING.matcher( line.trim() );
39
        final var match = attrs.matches();
40
41
        final var parser = match
42
          ? new OpeningParser( attrs.group( 1 ) )
43
          : new ClosingParser( line );
44
        final var divBlock = parser.getBlock();
45
46
        if( match ) {
47
          block.insertBefore( divBlock );
48
        }
49
        else {
50
          block.insertAfter( divBlock );
51
        }
52
53
        state.blockAdded( divBlock );
54
55
        // Schedule the line for removal (because it has been handled).
56
        sigilLines.add( line );
57
      }
58
    }
59
60
    sigilLines.forEach( lines::remove );
61
62
    return 0;
63
  }
64
}
165
A src/main/java/com/keenwrite/processors/markdown/extensions/fences/renderers/FencedDivRenderer.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.processors.markdown.extensions.fences.renderers;
3
4
import com.keenwrite.processors.markdown.extensions.fences.blocks.ClosingDivBlock;
5
import com.keenwrite.processors.markdown.extensions.fences.blocks.DivBlock;
6
import com.keenwrite.processors.markdown.extensions.fences.blocks.OpeningDivBlock;
7
import com.vladsch.flexmark.html.HtmlWriter;
8
import com.vladsch.flexmark.html.renderer.NodeRenderer;
9
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
10
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
11
import org.jetbrains.annotations.Nullable;
12
13
import java.util.Set;
14
15
/**
16
 * Responsible for rendering opening and closing fenced div blocks as HTML
17
 * {@code div} elements.
18
 */
19
public class FencedDivRenderer implements NodeRenderer {
20
  @Nullable
21
  @Override
22
  public Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
23
    return Set.of(
24
      new NodeRenderingHandler<>( OpeningDivBlock.class, this::render ),
25
      new NodeRenderingHandler<>( ClosingDivBlock.class, this::render )
26
    );
27
  }
28
29
  /**
30
   * Renders the fenced div block as an HTML {@code <div></div>} element.
31
   */
32
  void render(
33
    final DivBlock node,
34
    final NodeRendererContext context,
35
    final HtmlWriter html ) {
36
    node.write( html );
37
  }
38
}
139
M src/main/java/com/keenwrite/spelling/impl/Lexicon.java
133133
134134
  private static void status( final String s, final long count ) {
135
    clue( "Main.status.lexicon." + s, count );
135
    clue( STR."Main.status.lexicon.\{s}", count );
136136
  }
137137
}
M src/main/java/com/keenwrite/util/FileWalker.java
2929
    final Path path, final String glob, final Consumer<Path> c )
3030
    throws IOException {
31
    final var matcher = getDefault().getPathMatcher( "glob:" + glob );
31
    final var matcher = getDefault().getPathMatcher( STR."glob:\{glob}" );
3232
3333
    try( final var walk = Files.walk( path, 10 ) ) {
M src/main/resources/lexicons/en.txt
Binary file
A src/test/java/com/keenwrite/processors/markdown/extensions/ExtensionTest.java
1
package com.keenwrite.processors.markdown.extensions;
2
3
import com.vladsch.flexmark.html.HtmlRenderer;
4
import com.vladsch.flexmark.parser.Parser;
5
import com.vladsch.flexmark.parser.Parser.ParserExtension;
6
import org.junit.jupiter.api.TestInstance;
7
import org.junit.jupiter.params.ParameterizedTest;
8
import org.junit.jupiter.params.provider.Arguments;
9
import org.junit.jupiter.params.provider.MethodSource;
10
11
import java.util.LinkedList;
12
import java.util.List;
13
import java.util.stream.Stream;
14
15
import static org.junit.jupiter.api.Assertions.assertEquals;
16
17
@TestInstance( TestInstance.Lifecycle.PER_CLASS )
18
public abstract class ExtensionTest {
19
  private final List<ParserExtension> mExtensions = new LinkedList<>();
20
21
  @ParameterizedTest
22
  @MethodSource( "getDocuments" )
23
  public void test_Extensions_Markdown_Html(
24
    final String input, final String expected
25
  ) {
26
    final var pBuilder = Parser.builder();
27
    final var hBuilder = HtmlRenderer.builder();
28
    final var parser = pBuilder.extensions( mExtensions ).build();
29
    final var renderer = hBuilder.extensions( mExtensions ).build();
30
31
    final var document = parser.parse( input );
32
    final var actual = renderer.render( document );
33
34
    assertEquals( expected, actual );
35
  }
36
37
  protected void addExtension( final ParserExtension extension ) {
38
    mExtensions.add( extension );
39
  }
40
41
  protected Arguments args( final String in, final String out ) {
42
    return Arguments.of( in, out );
43
  }
44
45
  protected abstract Stream<Arguments> getDocuments();
46
}
147
A src/test/java/com/keenwrite/processors/markdown/extensions/fences/FencedDivExtensionTest.java
1
/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
2
 *
3
 * SPDX-License-Identifier: MIT
4
 */
5
package com.keenwrite.processors.markdown.extensions.fences;
6
7
import com.keenwrite.processors.markdown.extensions.ExtensionTest;
8
import org.junit.jupiter.api.BeforeAll;
9
import org.junit.jupiter.params.provider.Arguments;
10
11
import java.util.stream.Stream;
12
13
public class FencedDivExtensionTest extends ExtensionTest {
14
15
  @BeforeAll
16
  protected void setup() {
17
    addExtension( FencedDivExtension.create() );
18
  }
19
20
  @Override
21
  protected Stream<Arguments> getDocuments() {
22
    return Stream.of(
23
      args(
24
        """
25
          >
26
          > ::: {.concurrent title="3:58"}
27
          > Line 1
28
          > :::
29
          >
30
          > ::: {.concurrent title="3:59"}
31
          > Line 2
32
          > :::
33
          >
34
          > ::: {.concurrent title="4:00"}
35
          > Line 3
36
          > :::
37
          >
38
          """,
39
        """
40
          <blockquote>
41
          <div class="concurrent" data-title="3:58">
42
          <p>Line 1</p>
43
          </div><div class="concurrent" data-title="3:59">
44
          <p>Line 2</p>
45
          </div><div class="concurrent" data-title="4:00">
46
          <p>Line 3</p>
47
          </div>
48
          </blockquote>
49
          """
50
      ),
51
      args(
52
        """
53
          > Hello
54
          >
55
          > ::: world
56
          > Adventures
57
          >
58
          > in **absolute**
59
          >
60
          > nesting.
61
          > :::
62
          >
63
          > Goodbye
64
          """,
65
        """
66
          <blockquote>
67
          <p>Hello</p>
68
          <div class="world">
69
          <p>Adventures</p>
70
          <p>in <strong>absolute</strong></p>
71
          <p>nesting.</p>
72
          </div>
73
          <p>Goodbye</p>
74
          </blockquote>
75
          """
76
      )
77
    );
78
  }
79
}
180
M src/test/java/com/keenwrite/processors/markdown/extensions/references/CaptionsAndCrossReferencesExtensionTest.java
55
package com.keenwrite.processors.markdown.extensions.references;
66
7
import com.keenwrite.processors.markdown.extensions.ExtensionTest;
78
import com.keenwrite.processors.markdown.extensions.captions.CaptionExtension;
89
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
910
import com.keenwrite.processors.markdown.extensions.tex.TexExtension;
1011
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
1112
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
1213
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
1314
import com.vladsch.flexmark.ext.tables.TablesExtension;
14
import com.vladsch.flexmark.html.HtmlRenderer;
15
import com.vladsch.flexmark.parser.Parser;
16
import com.vladsch.flexmark.parser.Parser.ParserExtension;
17
import org.junit.jupiter.params.ParameterizedTest;
15
import org.junit.jupiter.api.BeforeAll;
1816
import org.junit.jupiter.params.provider.Arguments;
19
import org.junit.jupiter.params.provider.MethodSource;
2017
21
import java.util.LinkedList;
22
import java.util.List;
2318
import java.util.stream.Stream;
2419
2520
import static com.keenwrite.ExportFormat.XHTML_TEX;
2621
import static com.keenwrite.processors.ProcessorContext.Mutator;
2722
import static com.keenwrite.processors.ProcessorContext.builder;
28
import static org.junit.jupiter.api.Assertions.assertEquals;
2923
3024
@SuppressWarnings( "SpellCheckingInspection" )
31
public class CaptionsAndCrossReferencesExtensionTest {
32
  @ParameterizedTest
33
  @MethodSource( "testDocuments" )
34
  public void test_References_Documents_Html(
35
    final String input, final String expected
36
  ) {
37
    final var pBuilder = Parser.builder();
38
    final var hBuilder = HtmlRenderer.builder();
39
    final var extensions = createExtensions();
40
    final var parser = pBuilder.extensions( extensions ).build();
41
    final var renderer = hBuilder.extensions( extensions ).build();
42
43
    final var document = parser.parse( input );
44
    final var actual = renderer.render( document );
45
46
    assertEquals( expected, actual );
47
  }
48
49
  private static Stream<Arguments> testDocuments() {
25
public class CaptionsAndCrossReferencesExtensionTest extends ExtensionTest {
26
  protected Stream<Arguments> getDocuments() {
5027
    return Stream.of(
5128
      args(
...
9269
          measured by the ingenuity of inventions, but by humanity's ability
9370
          to anticipate and forfend dire aftermaths *before* using them.
94
71
          
9572
          [@note:advancement]
96
73
          
9774
          To what end?
9875
          """,
...
124101
        """
125102
          $$E=mc^2$$
126
103
          
127104
          :: Caption {#eqn:energy}
128105
          """,
...
137114
          main :: IO ()
138115
          ```
139
116
          
140117
          :: Source code caption {#listing:haskell1}
141118
          """,
...
150127
          ::: warning
151128
          Do not eat processed **sugar**.
152
129
          
153130
          Seriously.
154131
          :::
155
132
          
156133
          :: Caption {#warning:sugar}
157134
          """,
...
166143
        """
167144
          ![alt text](tunnel)
168
145
          
169146
          :: Caption {#fig:label}
170147
          """,
...
177154
        """
178155
          ![kitteh](kitten)
179
156
          
180157
          :: Caption **bold** {#fig:label} *italics*
181158
          """,
...
192169
          >
193170
          > I've traded my halo for horns and a whip.
194
171
          
195172
          :: Meschiya Lake - Lucky Devil {#lyrics:blues}
196173
          """,
...
210187
          | 1 | 2 | 3 |
211188
          | 4 | 5 | 6 |
212
189
          
213190
          :: Caption {#tbl:label}
214191
          """,
...
234211
          @enduml
235212
          ```
236
213
          
237214
          :: Diagram {#dia:seq1}
238215
          """,
...
253230
          Gas on down to future town,
254231
          Make prophecy take hold.
255
232
          
256233
          Warnin' sign, cent'ry old:
257234
          When buyin' coal, air is sold.
...
274251
      )
275252
    );
276
  }
277
278
  private static Arguments args( final String in, final String out ) {
279
    return Arguments.of( in, out );
280253
  }
281254
282
  private List<ParserExtension> createExtensions() {
283
    final var extensions = new LinkedList<ParserExtension>();
255
  @BeforeAll
256
  protected void setup() {
284257
    final var context = builder()
285258
      .with( Mutator::setExportFormat, XHTML_TEX )
286259
      .build();
287
288
    extensions.add( TexExtension.create( s -> s, context ) );
289
    extensions.add( DefinitionExtension.create() );
290
    extensions.add( StrikethroughSubscriptExtension.create() );
291
    extensions.add( SuperscriptExtension.create() );
292
    extensions.add( TablesExtension.create() );
293
    extensions.add( FencedDivExtension.create() );
294
    extensions.add( CrossReferenceExtension.create() );
295
    extensions.add( CaptionExtension.create() );
296260
297
    return extensions;
261
    addExtension( TexExtension.create( s -> s, context ) );
262
    addExtension( DefinitionExtension.create() );
263
    addExtension( StrikethroughSubscriptExtension.create() );
264
    addExtension( SuperscriptExtension.create() );
265
    addExtension( TablesExtension.create() );
266
    addExtension( FencedDivExtension.create() );
267
    addExtension( CrossReferenceExtension.create() );
268
    addExtension( CaptionExtension.create() );
298269
  }
299270
}