Dave Jarvis' Repositories

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

Render Markdown output from inline R functions to HTML

AuthorDaveJarvis <email>
Date2020-10-24 17:29:11 GMT-0700
Commit6be3332c6fe58e81e7e9216d89c7fb1e6c50b7a5
Parentab0d03e
Delta89 lines added, 55 lines removed, 34-line increase
src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
}
+ public static MarkdownProcessor create( final ProcessorContext context ) {
+ return create( IdentityProcessor.INSTANCE, context );
+ }
+
public static MarkdownProcessor create(
final Processor<String> successor, final Path path ) {
public Node toNode( final String markdown ) {
return parse( markdown );
+ }
+
+ /**
+ * Returns the result of converting the given AST into an HTML string.
+ *
+ * @param node The AST {@link Node} to convert to an HTML string.
+ * @return The given {@link Node} as an HTML string.
+ */
+ public String toHtml( final Node node ) {
+ return getRenderer().render( node );
}
*/
private String toHtml( final String markdown ) {
- return getRenderer().render( parse( markdown ) );
+ return toHtml( parse( markdown ) );
}
src/main/java/com/keenwrite/processors/InlineRProcessor.java
package com.keenwrite.processors;
-import com.keenwrite.StatusBarNotifier;
import com.keenwrite.preferences.UserPreferences;
+import com.keenwrite.processors.markdown.MarkdownProcessor;
+import com.vladsch.flexmark.ast.Paragraph;
+import com.vladsch.flexmark.ast.Text;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import static com.keenwrite.Constants.STATUS_PARSE_ERROR;
+import static com.keenwrite.StatusBarNotifier.clue;
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
import static com.keenwrite.sigils.RSigilOperator.PREFIX;
*/
private static final int MAX_CACHED_R_STATEMENTS = 512;
+
+ private final MarkdownProcessor mMarkdownProcessor;
/**
* Where to put document inline evaluated R expressions.
*/
- private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
+ private final Map<String, String> mEvalCache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(
- final Map.Entry<String, Object> eldest ) {
+ final Map.Entry<String, String> eldest ) {
return size() > MAX_CACHED_R_STATEMENTS;
}
*
* @param successor Subsequent link in the processing chain.
- * @param map Resolved definitions map.
+ * @param context Contains resolved definitions map.
*/
public InlineRProcessor(
final Processor<String> successor,
- final Map<String, String> map ) {
- super( successor, map );
+ final ProcessorContext context ) {
+ super( successor, context );
+
+ mMarkdownProcessor = MarkdownProcessor.create( context );
bootstrapScriptProperty().addListener(
- ( ob, oldScript, newScript ) -> setDirty( true ) );
+ ( __, oldScript, newScript ) -> setDirty( true ) );
workingDirectoryProperty().addListener(
- ( ob, oldScript, newScript ) -> setDirty( true ) );
+ ( __, oldScript, newScript ) -> setDirty( true ) );
getUserPreferences().addSaveEventHandler( ( handler ) -> {
while( currIndex >= 0 ) {
- // Copy everything up to, but not including, an R statement (`r#).
+ // Copy everything up to, but not including, the opening token.
sb.append( text, prevIndex, currIndex );
// Jump to the start of the R statement.
prevIndex = currIndex + PREFIX_LENGTH;
- // Find the statement ending (`), without indexing past the text boundary.
+ // Find the closing token, without indexing past the text boundary.
currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
// Only evaluate inline R statements that have end delimiters.
if( currIndex > 1 ) {
// Extract the inline R statement to be evaluated.
final String r = text.substring( prevIndex, currIndex );
// Pass the R statement into the R engine for evaluation.
try {
- final Object result = evalText( r );
+ final var result = evalCached( r );
// Append the string representation of the result into the text.
sb.append( result );
} catch( final Exception e ) {
// If the string couldn't be parsed using R, append the statement
// that failed to parse, instead of its evaluated value.
sb.append( PREFIX ).append( r ).append( SUFFIX );
// Tell the user that there was a problem.
- StatusBarNotifier.clue( STATUS_PARSE_ERROR,
- e.getMessage(),
- currIndex );
+ clue( STATUS_PARSE_ERROR,
+ e.getMessage(),
+ currIndex );
}
* @return The object resulting from the evaluation.
*/
- private Object evalText( final String r ) {
- return mEvalCache.computeIfAbsent( r, v -> eval( r ) );
+ private String evalCached( final String r ) {
+ return mEvalCache.computeIfAbsent( r, v -> evalHtml( r ) );
+ }
+
+ /**
+ * Converts the given string to HTML, trimming new lines, and inlining
+ * the text if it is a paragraph. Otherwise, the resulting HTML is most likely
+ * complex (e.g., a Markdown table) and should be rendered as its HTML
+ * equivalent.
+ *
+ * @param r The R expression to evaluate then convert to HTML.
+ * @return The result from the R expression as an HTML element.
+ */
+ private String evalHtml( final String r ) {
+ final var markdown = eval( r );
+ var node = mMarkdownProcessor.toNode( markdown ).getFirstChild();
+
+ if( node != null && node.isOrDescendantOfType( Paragraph.class ) ) {
+ node = new Text( node.getChars() );
+ }
+
+ // Trimming prevents displaced commas and unwanted newlines.
+ return mMarkdownProcessor.toHtml( node ).trim();
}
/**
* Evaluate an R expression and return the resulting object.
*
* @param r The expression to evaluate.
* @return The object resulting from the evaluation.
*/
- private Object eval( final String r ) {
+ private String eval( final String r ) {
try {
- return getScriptEngine().eval( r );
+ return ENGINE.eval( r ).toString();
} catch( final Exception ex ) {
- final String expr = r.substring( 0, min( r.length(), 30 ) );
- StatusBarNotifier.clue( "Main.status.error.r", expr, ex.getMessage() );
+ final var expr = r.substring( 0, min( r.length(), 30 ) );
+ clue( "Main.status.error.r", expr, ex.getMessage() );
+ return "";
}
-
- return "";
}
private UserPreferences getUserPreferences() {
return UserPreferences.getInstance();
- }
-
- private ScriptEngine getScriptEngine() {
- return ENGINE;
}
}
src/main/java/com/keenwrite/processors/ProcessorContext.java
}
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
Map<String, String> getResolvedMap() {
return mResolvedMap;
src/main/java/com/keenwrite/processors/ProcessorFactory.java
import java.nio.file.Path;
-import java.util.Map;
import static com.keenwrite.ExportFormat.NONE;
/**
* Responsible for creating processors capable of parsing, transforming,
* interpolating, and rendering known file types.
*/
public class ProcessorFactory extends AbstractFileFactory {
- private final ProcessorContext mProcessorContext;
+ private final ProcessorContext mContext;
/**
* Constructs a factory with the ability to create processors that can perform
* text and caret processing to generate a final preview.
*
- * @param processorContext Parameters needed to construct various processors.
+ * @param context Parameters needed to construct various processors.
*/
- private ProcessorFactory( final ProcessorContext processorContext ) {
- mProcessorContext = processorContext;
+ private ProcessorFactory( final ProcessorContext context ) {
+ mContext = context;
}
private Processor<String> createDefinitionProcessor(
final Processor<String> successor ) {
- return new DefinitionProcessor( successor, getResolvedMap() );
+ return new DefinitionProcessor( successor, getProcessorContext() );
}
private Processor<String> createRProcessor(
final Processor<String> successor ) {
- final var irp = new InlineRProcessor( successor, getResolvedMap() );
- final var rvp = new RVariableProcessor( irp, getResolvedMap() );
+ final var irp = new InlineRProcessor( successor, getProcessorContext() );
+ final var rvp = new RVariableProcessor( irp, getProcessorContext() );
return MarkdownProcessor.create( rvp, getProcessorContext() );
}
protected Processor<String> createRXMLProcessor(
final Processor<String> successor ) {
- final var xmlp = new XmlProcessor( successor, getPath() );
+ final var xmlp = new XmlProcessor( successor, getProcessorContext() );
return createRProcessor( xmlp );
}
private Processor<String> createXMLProcessor(
final Processor<String> successor ) {
- final var xmlp = new XmlProcessor( successor, getPath() );
+ final var xmlp = new XmlProcessor( successor, getProcessorContext() );
return createDefinitionProcessor( xmlp );
}
private Processor<String> createPreformattedProcessor(
final Processor<String> successor ) {
return new PreformattedProcessor( successor );
}
private ProcessorContext getProcessorContext() {
- return mProcessorContext;
+ return mContext;
}
private HTMLPreviewPane getPreviewPane() {
return getProcessorContext().getPreviewPane();
- }
-
- /**
- * Returns the variable map of interpolated definitions.
- *
- * @return A map to help dereference variables.
- */
- private Map<String, String> getResolvedMap() {
- return getProcessorContext().getResolvedMap();
}
src/main/java/com/keenwrite/processors/RVariableProcessor.java
public RVariableProcessor(
- final Processor<String> rp, final Map<String, String> map ) {
- super( rp, map );
+ final InlineRProcessor irp, final ProcessorContext context ) {
+ super( irp, context );
}
// Replace up to 32 occurrences before the string reallocates its buffer.
- final StringBuilder sb = new StringBuilder( length + 32 );
+ final var sb = new StringBuilder( length + 32 );
while( end >= 0 ) {
src/main/java/com/keenwrite/processors/XmlProcessor.java
* that they must be in the same directory.
*
- * @param processor Next link in the processing chain.
- * @param path The path to the XML file content to be processed.
+ * @param successor Next link in the processing chain.
+ * @param context Contains path to the XML file content to be processed.
*/
- public XmlProcessor( final Processor<String> processor, final Path path ) {
- super( processor );
- setPath( path );
+ public XmlProcessor(
+ final Processor<String> successor,
+ final ProcessorContext context ) {
+ super( successor );
+ setPath( context.getPath() );
}