| Author | DaveJarvis <email> |
|---|---|
| Date | 2021-12-30 14:05:54 GMT-0800 |
| Commit | 68c15210b5bb6a76b71214f7a28b92215de96af6 |
| Parent | d14acf8 |
| Delta | 156 lines added, 79 lines removed, 77-line increase |
| 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(); | ||
| } | ||
| 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 ); | ||
| + } | ||
| } | ||
| 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(); | ||
| } | ||
| } | ||
| 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 ); | ||
| private String resolve( final String value ) { | ||
| - return replace( value, mContext.getResolvedMap() ); | ||
| + return replace( value, mContext.getInterpolatedMap() ); | ||
| } | ||
| 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 ); | ||