| 37 | 37 | Using Java, first follow these one-time setup steps: |
| 38 | 38 | |
| 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). | |
| 40 | 40 | * JavaFX, which is bundled with BellSoft's *Full version*, is required. |
| 41 | 41 | 1. Install the JRE (include JRE's `bin` directory in the `PATH` environment variable). |
| 5 | 5 | package com.keenwrite.commands; |
| 6 | 6 | |
| 7 | import com.keenwrite.io.SysFile; | |
| 7 | 8 | import com.keenwrite.util.AlphanumComparator; |
| 8 | 9 | import com.keenwrite.util.RangeValidator; |
| ... | ||
| 46 | 47 | mExtension = extension; |
| 47 | 48 | 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 ); | |
| 48 | 55 | } |
| 49 | 56 | |
| 50 | 57 | public String call() throws IOException { |
| 51 | final var glob = "**/*." + mExtension; | |
| 58 | final var glob = STR."**/*.\{mExtension}"; | |
| 52 | 59 | final var files = new ArrayList<Path>(); |
| 53 | 60 | final var text = new StringBuilder( DOCUMENT_LENGTH ); |
| 54 | 61 | final var chapter = new AtomicInteger(); |
| 55 | 62 | final var eol = lineSeparator(); |
| 56 | ||
| 57 | 63 | final var validator = new RangeValidator( mRange ); |
| 58 | 64 | |
| 59 | 65 | walk( mParent, glob, files::add ); |
| 60 | 66 | files.sort( new AlphanumComparator<>() ); |
| 61 | 67 | files.forEach( file -> { |
| 62 | 68 | 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 ) ) { | |
| 64 | 73 | clue( "Main.status.export.concat", file ); |
| 65 | 74 | |
| 40 | 40 | */ |
| 41 | 41 | public final class XhtmlProcessor extends ExecutorProcessor<String> { |
| 42 | private static final Curler sTypographer = | |
| 42 | private static final Curler sCurler = | |
| 43 | 43 | new Curler( createContractions(), FILTER_XML, true ); |
| 44 | 44 | |
| ... | ||
| 102 | 102 | final var curl = mContext.getCurlQuotes(); |
| 103 | 103 | |
| 104 | return curl ? sTypographer.apply( document ) : document; | |
| 104 | return curl ? sCurler.apply( document ) : document; | |
| 105 | 105 | } catch( final Exception ex ) { |
| 106 | 106 | clue( ex ); |
| 11 | 11 | import com.keenwrite.processors.variable.VariableProcessor; |
| 12 | 12 | 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; | |
| 14 | 14 | import com.keenwrite.processors.markdown.extensions.images.ImageLinkExtension; |
| 15 | 15 | import com.keenwrite.processors.markdown.extensions.outline.DocumentOutlineExtension; |
| ... | ||
| 84 | 84 | result.add( ImageLinkExtension.create( context ) ); |
| 85 | 85 | result.add( TexExtension.create( evaluator, context ) ); |
| 86 | result.add( FencedBlockExtension.create( processor, evaluator, context ) ); | |
| 86 | result.add( ImageBlockExtension.create( processor, evaluator, context ) ); | |
| 87 | 87 | |
| 88 | 88 | if( context.isExportFormat( ExportFormat.NONE ) ) { |
| 5 | 5 | package com.keenwrite.processors.markdown.extensions.captions; |
| 6 | 6 | |
| 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; | |
| 9 | 9 | import com.vladsch.flexmark.parser.block.NodePostProcessor; |
| 10 | 10 | import com.vladsch.flexmark.util.ast.Node; |
| 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 | } | |
| 15 | 1 |
| 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 | } | |
| 25 | 1 |
| 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 | } | |
| 349 | 1 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 1 | /* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | 2 | package com.keenwrite.processors.markdown.extensions.fences; |
| 3 | 3 | |
| 4 | import com.keenwrite.processors.markdown.extensions.common.MarkdownCustomBlockParserFactory; | |
| 4 | import com.keenwrite.processors.markdown.extensions.common.MarkdownParserExtension; | |
| 5 | 5 | 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; | |
| 6 | 9 | import com.vladsch.flexmark.html.renderer.NodeRendererFactory; |
| 7 | 10 | 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; | |
| 13 | 11 | |
| 14 | import java.util.ArrayList; | |
| 15 | 12 | import java.util.regex.Pattern; |
| 16 | 13 | |
| ... | ||
| 36 | 33 | * </p> |
| 37 | 34 | * <p> |
| 38 | * ::: {#verse .p .d k=v author="Emily Dickinson"} | |
| 35 | * ::: {#verse .p .d k=v author="Dickinson"} | |
| 39 | 36 | * Because I could not stop for Death -- |
| 40 | 37 | * He kindly stopped for me -- |
| ... | ||
| 47 | 44 | * The second example produces the following starting {@code div} element: |
| 48 | 45 | * </p> |
| 46 | * {@literal <div id="verse" class="p d" data-k="v" data-author="Dickson">} | |
| 47 | * | |
| 49 | 48 | * <p> |
| 50 | * <div id="verse" class="p d" data-k="v" data-author="Emily Dickson"> | |
| 49 | * This will parse fenced divs embedded inside of blockquote environments. | |
| 51 | 50 | * </p> |
| 52 | 51 | */ |
| 53 | public class FencedDivExtension extends MarkdownRendererExtension { | |
| 52 | public class FencedDivExtension extends MarkdownRendererExtension | |
| 53 | implements MarkdownParserExtension { | |
| 54 | 54 | /** |
| 55 | 55 | * Matches any number of colons at start of line. This will match both the |
| 56 | 56 | * opening and closing fences, with any number of colons. |
| 57 | 57 | */ |
| 58 | private static final Pattern FENCE = compile( "^:::.*" ); | |
| 58 | public static final Pattern FENCE = compile( "^:::+.*" ); | |
| 59 | 59 | |
| 60 | 60 | /** |
| 61 | 61 | * 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. | |
| 76 | 62 | */ |
| 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*$", | |
| 82 | 65 | UNICODE_CHARACTER_CLASS ); |
| 83 | 66 | |
| 84 | 67 | public static FencedDivExtension create() { |
| 85 | 68 | return new FencedDivExtension(); |
| 86 | 69 | } |
| 87 | 70 | |
| 88 | 71 | @Override |
| 89 | 72 | public void extend( final Builder builder ) { |
| 90 | builder.customBlockParserFactory( new DivBlockParserFactory() ); | |
| 73 | builder.customBlockParserFactory( new CustomDivBlockParserFactory() ); | |
| 74 | builder.paragraphPreProcessorFactory( new FencedDivPreProcessorFactory() ); | |
| 91 | 75 | } |
| 92 | 76 | |
| 93 | 77 | @Override |
| 94 | 78 | protected NodeRendererFactory createNodeRendererFactory() { |
| 95 | 79 | 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 | } | |
| 235 | 80 | } |
| 236 | 81 | } |
| 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 | } | |
| 14 | 1 |
| 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 | } | |
| 36 | 1 |
| 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 | } | |
| 1 | 349 |
| 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 | } | |
| 27 | 1 |
| 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 | } | |
| 1 | 20 |
| 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 | } | |
| 1 | 29 |
| 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 | } | |
| 1 | 30 |
| 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 | } | |
| 1 | 22 |
| 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 | } | |
| 1 | 61 |
| 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 | } | |
| 1 | 15 |
| 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 | } | |
| 1 | 40 |
| 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 | } | |
| 1 | 26 |
| 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 | } | |
| 1 | 24 |
| 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 | } | |
| 1 | 98 |
| 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 | } | |
| 1 | 65 |
| 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 | } | |
| 1 | 39 |
| 133 | 133 | |
| 134 | 134 | 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 ); | |
| 136 | 136 | } |
| 137 | 137 | } |
| 29 | 29 | final Path path, final String glob, final Consumer<Path> c ) |
| 30 | 30 | throws IOException { |
| 31 | final var matcher = getDefault().getPathMatcher( "glob:" + glob ); | |
| 31 | final var matcher = getDefault().getPathMatcher( STR."glob:\{glob}" ); | |
| 32 | 32 | |
| 33 | 33 | try( final var walk = Files.walk( path, 10 ) ) { |
| 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 | } | |
| 1 | 47 |
| 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 | } | |
| 1 | 80 |
| 5 | 5 | package com.keenwrite.processors.markdown.extensions.references; |
| 6 | 6 | |
| 7 | import com.keenwrite.processors.markdown.extensions.ExtensionTest; | |
| 7 | 8 | import com.keenwrite.processors.markdown.extensions.captions.CaptionExtension; |
| 8 | 9 | import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension; |
| 9 | 10 | import com.keenwrite.processors.markdown.extensions.tex.TexExtension; |
| 10 | 11 | import com.vladsch.flexmark.ext.definition.DefinitionExtension; |
| 11 | 12 | import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension; |
| 12 | 13 | import com.vladsch.flexmark.ext.superscript.SuperscriptExtension; |
| 13 | 14 | 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; | |
| 18 | 16 | import org.junit.jupiter.params.provider.Arguments; |
| 19 | import org.junit.jupiter.params.provider.MethodSource; | |
| 20 | 17 | |
| 21 | import java.util.LinkedList; | |
| 22 | import java.util.List; | |
| 23 | 18 | import java.util.stream.Stream; |
| 24 | 19 | |
| 25 | 20 | import static com.keenwrite.ExportFormat.XHTML_TEX; |
| 26 | 21 | import static com.keenwrite.processors.ProcessorContext.Mutator; |
| 27 | 22 | import static com.keenwrite.processors.ProcessorContext.builder; |
| 28 | import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 29 | 23 | |
| 30 | 24 | @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() { | |
| 50 | 27 | return Stream.of( |
| 51 | 28 | args( |
| ... | ||
| 92 | 69 | measured by the ingenuity of inventions, but by humanity's ability |
| 93 | 70 | to anticipate and forfend dire aftermaths *before* using them. |
| 94 | ||
| 71 | ||
| 95 | 72 | [@note:advancement] |
| 96 | ||
| 73 | ||
| 97 | 74 | To what end? |
| 98 | 75 | """, |
| ... | ||
| 124 | 101 | """ |
| 125 | 102 | $$E=mc^2$$ |
| 126 | ||
| 103 | ||
| 127 | 104 | :: Caption {#eqn:energy} |
| 128 | 105 | """, |
| ... | ||
| 137 | 114 | main :: IO () |
| 138 | 115 | ``` |
| 139 | ||
| 116 | ||
| 140 | 117 | :: Source code caption {#listing:haskell1} |
| 141 | 118 | """, |
| ... | ||
| 150 | 127 | ::: warning |
| 151 | 128 | Do not eat processed **sugar**. |
| 152 | ||
| 129 | ||
| 153 | 130 | Seriously. |
| 154 | 131 | ::: |
| 155 | ||
| 132 | ||
| 156 | 133 | :: Caption {#warning:sugar} |
| 157 | 134 | """, |
| ... | ||
| 166 | 143 | """ |
| 167 | 144 |  |
| 168 | ||
| 145 | ||
| 169 | 146 | :: Caption {#fig:label} |
| 170 | 147 | """, |
| ... | ||
| 177 | 154 | """ |
| 178 | 155 |  |
| 179 | ||
| 156 | ||
| 180 | 157 | :: Caption **bold** {#fig:label} *italics* |
| 181 | 158 | """, |
| ... | ||
| 192 | 169 | > |
| 193 | 170 | > I've traded my halo for horns and a whip. |
| 194 | ||
| 171 | ||
| 195 | 172 | :: Meschiya Lake - Lucky Devil {#lyrics:blues} |
| 196 | 173 | """, |
| ... | ||
| 210 | 187 | | 1 | 2 | 3 | |
| 211 | 188 | | 4 | 5 | 6 | |
| 212 | ||
| 189 | ||
| 213 | 190 | :: Caption {#tbl:label} |
| 214 | 191 | """, |
| ... | ||
| 234 | 211 | @enduml |
| 235 | 212 | ``` |
| 236 | ||
| 213 | ||
| 237 | 214 | :: Diagram {#dia:seq1} |
| 238 | 215 | """, |
| ... | ||
| 253 | 230 | Gas on down to future town, |
| 254 | 231 | Make prophecy take hold. |
| 255 | ||
| 232 | ||
| 256 | 233 | Warnin' sign, cent'ry old: |
| 257 | 234 | When buyin' coal, air is sold. |
| ... | ||
| 274 | 251 | ) |
| 275 | 252 | ); |
| 276 | } | |
| 277 | ||
| 278 | private static Arguments args( final String in, final String out ) { | |
| 279 | return Arguments.of( in, out ); | |
| 280 | 253 | } |
| 281 | 254 | |
| 282 | private List<ParserExtension> createExtensions() { | |
| 283 | final var extensions = new LinkedList<ParserExtension>(); | |
| 255 | @BeforeAll | |
| 256 | protected void setup() { | |
| 284 | 257 | final var context = builder() |
| 285 | 258 | .with( Mutator::setExportFormat, XHTML_TEX ) |
| 286 | 259 | .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() ); | |
| 296 | 260 | |
| 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() ); | |
| 298 | 269 | } |
| 299 | 270 | } |