Dave Jarvis' Repositories

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

Simplify R processing logic

AuthorDaveJarvis <email>
Date2021-12-30 14:05:54 GMT-0800
Commit68c15210b5bb6a76b71214f7a28b92215de96af6
Parentd14acf8
Delta156 lines added, 79 lines removed, 77-line increase
src/test/java/com/keenwrite/definition/TreeViewTest.java
import com.keenwrite.preferences.XmlStore;
import com.keenwrite.preview.HtmlPreview;
-import com.keenwrite.sigils.Sigils;
-import com.keenwrite.sigils.YamlSigilOperator;
import com.panemu.tiwulfx.control.dock.DetachableTabPane;
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.property.SimpleStringProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import org.testfx.framework.junit5.Start;
-import static com.keenwrite.constants.Constants.DEF_DELIM_BEGAN_DEFAULT;
-import static com.keenwrite.constants.Constants.DEF_DELIM_ENDED_DEFAULT;
import static com.keenwrite.util.FontLoader.initFonts;
final var workspace = new Workspace( store );
final var mainPane = new SplitPane();
-
- final var began = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT );
- final var ended = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT );
- final var sigils = new Sigils( began.get(), ended.get() );
- final var operator = new YamlSigilOperator( sigils );
final var transformer = new YamlTreeTransformer();
- final var editor = new DefinitionEditor( transformer, operator );
+ final var editor = new DefinitionEditor( transformer );
final var tabPane1 = new DetachableTabPane();
tabPane1.addTab( "Editor", editor );
final var tabPane2 = new DetachableTabPane();
- final var tab21 = tabPane2.addTab( "Picker", new ColorPicker() );
- final var tab22 = tabPane2.addTab( "Editor",
- new MarkdownEditor( workspace ) );
+ final var tab21 =
+ tabPane2.addTab( "Picker", new ColorPicker() );
+ final var tab22 =
+ tabPane2.addTab( "Editor", new MarkdownEditor( workspace ) );
tab21.setTooltip( new Tooltip( "Colour Picker" ) );
tab22.setTooltip( new Tooltip( "Text Editor" ) );
final var tabPane3 = new DetachableTabPane();
tabPane3.addTab( "Preview", new HtmlPreview( workspace ) );
editor.addTreeChangeHandler( mTreeHandler );
mainPane.getItems().addAll( tabPane1, tabPane2, tabPane3 );
-
- final var scene = new Scene( mainPane );
- stage.setScene( scene );
+ stage.setScene( new Scene( mainPane ) );
stage.show();
}
src/main/java/com/keenwrite/util/InterpolatingMap.java
import com.keenwrite.sigils.SigilOperator;
-import com.keenwrite.sigils.Sigils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
private static final int INITIAL_CAPACITY = 1 << 8;
- public InterpolatingMap() {
- super( INITIAL_CAPACITY );
- }
+ private final SigilOperator mOperator;
/**
- * Interpolates all values in the map that reference other values by way
- * of key names. Performs a non-greedy match of key names delimited by
- * definition tokens. This operation modifies the map directly.
- *
* @param operator Contains the opening and closing sigils that mark
* where variable names begin and end.
- * @return {@code this}
*/
- public Map<String, String> interpolate( final SigilOperator operator ) {
- sigilize( operator );
- interpolate( operator.getSigils() );
- return this;
+ public InterpolatingMap( final SigilOperator operator ) {
+ super( INITIAL_CAPACITY );
+
+ assert operator != null;
+ mOperator = operator;
}
/**
- * Wraps each key in this map with the starting and ending sigils provided
- * by the given {@link SigilOperator}. This operation modifies the map
- * directly.
- *
- * @param operator Container for starting and ending sigils.
+ * @param operator Contains the opening and closing sigils that mark
+ * where variable names begin and end.
+ * @param m The initial {@link Map} to copy into this instance.
*/
- private void sigilize( final SigilOperator operator ) {
- forEach( ( k, v ) -> put( operator.entoken( k ), v ) );
+ public InterpolatingMap(
+ final SigilOperator operator, final Map<String, String> m ) {
+ this( operator );
+ putAll( m );
}
/**
* Interpolates all values in the map that reference other values by way
* of key names. Performs a non-greedy match of key names delimited by
* definition tokens. This operation modifies the map directly.
*
- * @param sigils Contains the opening and closing sigils that mark
- * where variable names begin and end.
+ * @return The number of failed substitutions.
*/
- private void interpolate( final Sigils sigils ) {
+ public int interpolate() {
+ final var sigils = mOperator.getSigils();
final var pattern = compile(
format(
- "(%s.*?%s)", quote( sigils.getBegan() ), quote( sigils.getEnded() )
+ "%s(.*?)%s", quote( sigils.getBegan() ), quote( sigils.getEnded() )
)
);
- replaceAll( ( k, v ) -> resolve( v, pattern ) );
+ final var failures = new AtomicInteger();
+
+ for( final var k : keySet() ) {
+ replace( k, interpolate( get( k ), pattern, failures ) );
+ }
+
+ return failures.get();
}
/**
* Given a value with zero or more key references, this will resolve all
* the values, recursively. If a key cannot be de-referenced, the value will
- * contain the key name.
+ * contain the key name, including the original sigils.
*
- * @param value Value containing zero or more key references.
- * @param pattern The regular expression pattern to match variable key names.
+ * @param value Value containing zero or more key references.
+ * @param pattern The regular expression pattern to match variable key names.
+ * @param failures Incremented when a variable replacement fails.
* @return The given value with all embedded key references interpolated.
*/
- private String resolve( String value, final Pattern pattern ) {
+ private String interpolate(
+ String value, final Pattern pattern, final AtomicInteger failures ) {
+ assert value != null;
+ assert pattern != null;
+
final var matcher = pattern.matcher( value );
while( matcher.find() ) {
final var keyName = matcher.group( GROUP_DELIMITED );
final var mapValue = get( keyName );
- final var keyValue = mapValue == null
- ? keyName
- : resolve( mapValue, pattern );
- value = value.replace( keyName, keyValue );
+ if( mapValue == null ) {
+ failures.incrementAndGet();
+ }
+ else {
+ final var keyValue = interpolate( mapValue, pattern, failures );
+ value = value.replace( mOperator.entoken( keyName ), keyValue );
+ }
}
src/main/java/com/keenwrite/sigils/Sigils.java
public Sigils( final String began, final String ended ) {
super( began, ended );
+
+ assert began != null;
+ assert !began.isBlank();
+ assert ended != null;
+ assert !ended.isBlank();
}
public String getEnded() {
return getValue();
+ }
+
+ @Override
+ public String toString() {
+ return getBegan() + getEnded();
}
}
src/main/java/com/keenwrite/processors/r/RVariableProcessor.java
package com.keenwrite.processors.r;
-import com.keenwrite.processors.DefinitionProcessor;
+import com.keenwrite.processors.VariableProcessor;
import com.keenwrite.processors.ProcessorContext;
import com.keenwrite.sigils.SigilOperator;
* {@code v$tree$leaf}.
*/
-public final class RVariableProcessor extends DefinitionProcessor {
+public final class RVariableProcessor extends VariableProcessor {
- private final SigilOperator mSigilOperator;
+ private final SigilOperator mOperator;
public RVariableProcessor(
final InlineRProcessor irp, final ProcessorContext context ) {
super( irp, context );
- mSigilOperator = context.getWorkspace().createRSigilOperator();
+ mOperator = context.getWorkspace().createRSigilOperator();
}
final var rMap = new HashMap<String, String>( map.size() );
- for( final var entry : map.entrySet() ) {
- final var key = entry.getKey();
- rMap.put( mSigilOperator.entoken( key ), escape( map.get( key ) ) );
- }
+ map.forEach( ( k, v ) -> rMap.put( mOperator.entoken( k ), escape( v ) ) );
return rMap;
private String escape(
final String haystack, final char needle, final String thread ) {
+ assert haystack != null;
+ assert thread != null;
+
int end = haystack.indexOf( needle );
src/main/java/com/keenwrite/processors/XhtmlProcessor.java
private String resolve( final String value ) {
- return replace( value, mContext.getResolvedMap() );
+ return replace( value, mContext.getInterpolatedMap() );
}
src/main/java/com/keenwrite/preferences/XmlStore.java
import javafx.beans.property.SetProperty;
import org.w3c.dom.Document;
+import org.w3c.dom.Element;
import org.w3c.dom.Node;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
import java.util.*;
import java.util.Map.Entry;
private static Document load( final File config ) {
+ assert config != null;
+
try {
return DocumentParser.parse( config );
*
* @param key {@link Key} name to retrieve.
- * @return The value associated with the key, or the empty string if the
- * key name could not be compiled.
+ * @return The value associated with the key.
+ * @throws NoSuchElementException No value could be found for the key.
*/
- public String getValue( final Key key ) {
+ public String getValue( final Key key ) throws NoSuchElementException {
assert key != null;
try {
final var xpath = toXPath( key );
final var expr = DocumentParser.compile( xpath );
- return expr.evaluate( mDocument );
- } catch( final XPathExpressionException ignored ) {
- // This exception is a programming error; return a default value.
- return "";
- }
+
+ if( expr.evaluate( mDocument, NODE ) instanceof Node node ) {
+ return node.getTextContent();
+ }
+ } catch( final XPathExpressionException ignored ) {}
+
+ throw new NoSuchElementException( key.toString() );
}
}
- public void save( final File config ) {
- System.out.println( "SAVE TO: " + config );
- System.out.println( DocumentParser.toString( mDocument ) );
+ /**
+ * Call to write the user preferences to a file.
+ *
+ * @param config The file wherein the preferences are saved.
+ * @throws IOException Could not write to the file.
+ */
+ public void save( final File config ) throws IOException {
+ assert config != null;
+
+ try( final var writer = new FileWriter( config ) ) {
+ writer.write( DocumentParser.toString( mDocument ) );
+ }
}
assert key != null;
assert set != null;
+
+ Node node = null;
try {
- final var node = upsert( key, mDocument );
+ for( final var item : set ) {
+ if( node == null ) {
+ node = upsert( key, mDocument );
+ }
+ else {
+ final var doc = node.getOwnerDocument();
+ final var sibling = doc.createElement( key.name() );
+ var parent = node.getParentNode();
- // Add child nodes and values.
+ if( parent == null ) {
+ parent = doc.getDocumentElement();
+ }
- System.out.printf( "%s = %s%n", key, set );
+ parent.appendChild( sibling );
+ node = sibling;
+ }
+
+ node.setTextContent( item.toString() );
+ }
} catch( final XPathExpressionException ignored ) {}
}
/**
- * Finds the element in the document represented by the given {@link Key}.
- * If no element is found then the full path to the element is created.
+ * Provides the equivalent of update-or-insert behaviour provided by some
+ * SQL databases. Finds the element in the document represented by the
+ * given {@link Key}. If no element is found then the full path to the
+ * element is created. In essence, this method converts a hierarchy of
+ * {@link Key} names into a hierarchy of {@link Document} {@link Element}s
+ * (i.e., {@link Node}s).
+ * <p>
+ * For example, given a key named {@code workspace.meta.version}, this will
+ * produce a document structure that, when exported as XML, resembles:
+ * <pre>{@code
+ * <root>
+ * <workspace>
+ * <meta>
+ * <version/>
+ * </meta>
+ * </workspace>
+ * </root>
+ * }</pre>
+ * <p>
+ * The calling code is responsible for populating the {@link Node} returned
+ * with its particular value. In the example above, the text content of the
+ * {@link Node} would be filled with the application version number.
*
* @param key The application key representing a user preference.
* @param doc The document that may contain an xpath for the {@link Key}.
* @return The existing or new element.
*/
private Node upsert( final Key key, final Document doc )
throws XPathExpressionException {
+ assert key != null;
+ assert doc != null;
+
final var missing = new Stack<Key>();
Key visitor = key;
Node parent = null;
do {
final var xpath = toXPath( visitor );
final var expr = DocumentParser.compile( xpath );
final var element = expr.evaluate( doc, NODE );
- // If an element exists on the first iteration, return it.
+ // If an element exists on the first iteration, return it because there
+ // is no missing hierarchy to create.
if( element instanceof Node node ) {
if( missing.isEmpty() ) {
}
}
- while( visitor.hasParent() && parent == null );
+ while( visitor != null && parent == null );
- // If the document is empty, start creating nodes at the document root.
+ // If the document is empty, update the top-level document element.
if( parent == null ) {
parent = doc.getDocumentElement();
+
+ // If there is still no top-level element, then create it.
+ if( parent == null ) {
+ parent = doc.createElement( mRoot );
+ doc.appendChild( parent );
+ }
}
+
+ assert parent != null;
// Create the hierarchy.
*/
private void visit( final Key key, final Consumer<Node> consumer ) {
+ assert key != null;
+ assert consumer != null;
+
try {
final var xpath = toXPath( key );
+
DocumentParser.visit( mDocument, xpath, consumer );
} catch( final XPathExpressionException ignored ) {
- // Programming error. Maybe triggered loading a previous config version?
+ // Programming error. Triggered by loading a previous config version?
}
}
private StringBuilder toXPath( final Key key )
throws XPathExpressionException {
+ assert key != null;
+
final var sb = new StringBuilder( 128 );