Dave Jarvis' Repositories

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

Fixed double-initialization bug. Refactored and reorganized code. Removed unused methods. Moved many constants into settings properties file. Removed now superfluous setUserData. Added toString methods to reference respective file names and paths. Simplified open files code to not return any values. Differentiate filter extension titles from filter extensions for reuse. Migrated TreeView into main window. Moved YAML processing into a DefinitionSource implementation.

Authordjarvis <email>
Date2016-12-13 20:40:44 GMT-0800
Commit5a3ec32754596b8706bc5890a4dba5a8ea85b58b
Parent912fad1
Delta465 lines added, 1750 lines removed, 1285-line decrease
src/main/resources/com/scrivenvar/settings.properties
# ########################################################################
#
+# Application
+#
+# ########################################################################
+
+application.title=scrivenvar
+application.package=com/${application.title}
+application.messages= com.${application.title}.messages
+
+# ########################################################################
+#
+# Preferences
+#
+# ########################################################################
+
+preferences.root=com.${application.title}
+preferences.root.state=state
+preferences.root.options=options
+
+# ########################################################################
+#
+# File References
+#
+# ########################################################################
+
+file.stylesheet.scene=${application.package}/scene.css
+file.stylesheet.markdown=${application.package}/editor/Markdown.css
+file.stylesheet.preview=webview.css
+
+file.logo.16 =${application.package}/logo16.png
+file.logo.32 =${application.package}/logo32.png
+file.logo.128=${application.package}/logo128.png
+file.logo.256=${application.package}/logo256.png
+file.logo.512=${application.package}/logo512.png
+
+# ########################################################################
+#
+# Caret token
+#
+# ########################################################################
+caret.token.base=CARETPOSITION
+caret.token.markdown=%${constant.caret.token.base}%
+caret.token.xml=<![CDATA[${constant.caret.token.markdown}]]>
+caret.token.html=<span id="${caret.token.base}"></span>
+
+# ########################################################################
+#
# Filename Extensions
#
# ########################################################################
+
+# Comma-separated list of definition filename extensions.
+file.ext.definition.json=*.json
+file.ext.definition.toml=*.toml
+file.ext.definition.yaml=*.yml,*.yaml
+file.ext.definition.properties=*.properties,*.props
# Comma-separated list of filename extensions.
-Dialog.file.choose.filter.ext.markdown=*.Rmd,*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt
-Dialog.file.choose.filter.ext.definition=*.yml,*.yaml,*.properties,*.props
-Dialog.file.choose.filter.ext.xml=*.xml,*.Rxml
-Dialog.file.choose.filter.ext.all=*.*
+filter.file.ext.markdown=*.Rmd,*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt
+filter.file.ext.definition=${file.ext.definition.yaml}
+filter.file.ext.xml=*.xml,*.Rxml
+filter.file.ext.all=*.*
# ########################################################################
src/main/java/com/scrivenvar/yaml/YamlTreeAdapter.java
-/*
- * Copyright 2016 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.yaml;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.scrivenvar.ui.VariableTreeItem;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Map.Entry;
-import javafx.scene.control.TreeItem;
-import javafx.scene.control.TreeView;
-
-/**
- * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
- * interface.
- *
- * @author White Magic Software, Ltd.
- */
-public class YamlTreeAdapter {
-
- private YamlParser yamlParser;
-
- public YamlTreeAdapter( final YamlParser parser ) {
- setYamlParser( parser );
- }
-
- /**
- * Converts a YAML document to a TreeView based on the document keys. Only the
- * first document in the stream is adapted. This does not close the stream.
- *
- * @param in Contains a YAML document.
- * @param name Name of the root TreeItem.
- *
- * @return A TreeView populated with all the keys in the YAML document.
- *
- * @throws IOException Could not read from the stream.
- */
- public TreeView<String> adapt( final InputStream in, final String name )
- throws IOException {
-
- final JsonNode rootNode = getYamlParser().process( in );
- final TreeItem<String> rootItem = createTreeItem( name );
-
- rootItem.setExpanded( true );
- adapt( rootNode, rootItem );
- return new TreeView<>( rootItem );
- }
-
- /**
- * Iterate over a given root node (at any level of the tree) and adapt each
- * leaf node.
- *
- * @param rootNode A JSON node (YAML node) to adapt.
- * @param rootItem The tree item to use as the root when processing the node.
- */
- private void adapt(
- final JsonNode rootNode, final TreeItem<String> rootItem ) {
-
- rootNode.fields().forEachRemaining(
- (Entry<String, JsonNode> leaf) -> adapt( leaf, rootItem )
- );
- }
-
- /**
- * Recursively adapt each rootNode to a corresponding rootItem.
- *
- * @param rootNode The node to adapt.
- * @param rootItem The item to adapt using the node's key.
- */
- private void adapt(
- final Entry<String, JsonNode> rootNode, final TreeItem<String> rootItem ) {
-
- final JsonNode leafNode = rootNode.getValue();
- final String key = rootNode.getKey();
- final TreeItem<String> leaf = createTreeItem( key );
-
- if( leafNode.isValueNode() ) {
- leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
- }
-
- rootItem.getChildren().add( leaf );
-
- if( leafNode.isObject() ) {
- adapt( leafNode, leaf );
- }
- }
-
- /**
- * Creates a new tree item that can be added to the tree view.
- *
- * @param value The node's value.
- *
- * @return A new tree item node, never null.
- */
- private TreeItem<String> createTreeItem( final String value ) {
- return new VariableTreeItem<>( value );
- }
-
- private YamlParser getYamlParser() {
- return this.yamlParser;
- }
-
- private void setYamlParser( final YamlParser yamlParser ) {
- this.yamlParser = yamlParser;
- }
-
-}
src/main/java/com/scrivenvar/test/TestVariableNameProcessor.java
+/*
+ * Copyright 2016 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.test;
+
+import com.scrivenvar.definition.VariableTreeItem;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import static java.util.concurrent.ThreadLocalRandom.current;
+import java.util.concurrent.TimeUnit;
+import static java.util.concurrent.TimeUnit.DAYS;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static javafx.application.Application.launch;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.stage.Stage;
+import org.ahocorasick.trie.*;
+import org.ahocorasick.trie.Trie.TrieBuilder;
+import static org.apache.commons.lang.RandomStringUtils.randomNumeric;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * Tests substituting variable definitions with their values in a swath of text.
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class TestVariableNameProcessor extends TestHarness {
+
+ private final static int TEXT_SIZE = 1000000;
+ private final static int MATCHES_DIVISOR = 1000;
+
+ private final static StringBuilder SOURCE
+ = new StringBuilder( randomNumeric( TEXT_SIZE ) );
+
+ private final static boolean DEBUG = false;
+
+ public TestVariableNameProcessor() {
+ }
+
+ @Override
+ public void start( final Stage stage ) throws Exception {
+ super.start( stage );
+
+ final TreeView<String> treeView = createTreeView();
+ final Map<String, String> definitions = new HashMap<>();
+
+ populate( treeView.getRoot(), definitions );
+ injectVariables( definitions );
+
+ final String text = SOURCE.toString();
+
+ show( text );
+
+ long duration = System.nanoTime();
+
+ // TODO: Test replaceEach (with intercoluated variables) and replaceEachRepeatedly
+ // (without intercoluation).
+ final String result = testBorAhoCorasick( text, definitions );
+
+ duration = System.nanoTime() - duration;
+
+ show( result );
+ System.out.println( elapsed( duration ) );
+
+ System.exit( 0 );
+ }
+
+ private void show( final String s ) {
+ if( DEBUG ) {
+ System.out.printf( "%s\n\n", s );
+ }
+ }
+
+ private String testBorAhoCorasick(
+ final String text,
+ final Map<String, String> definitions ) {
+ // Create a buffer sufficiently large that re-allocations are minimized.
+ final StringBuilder sb = new StringBuilder( text.length() << 1 );
+
+ final TrieBuilder builder = Trie.builder();
+ builder.onlyWholeWords();
+ builder.removeOverlaps();
+
+ final String[] keys = keys( definitions );
+
+ for( final String key : keys ) {
+ builder.addKeyword( key );
+ }
+
+ final Trie trie = builder.build();
+ final Collection<Emit> emits = trie.parseText( text );
+
+ int prevIndex = 0;
+
+ for( final Emit emit : emits ) {
+ final int matchIndex = emit.getStart();
+
+ sb.append( text.substring( prevIndex, matchIndex ) );
+ sb.append( definitions.get( emit.getKeyword() ) );
+ prevIndex = emit.getEnd() + 1;
+ }
+
+ // Add the remainder of the string (contains no more matches).
+ sb.append( text.substring( prevIndex ) );
+
+ return sb.toString();
+ }
+
+ private String testStringUtils(
+ final String text, final Map<String, String> definitions ) {
+ final String[] keys = keys( definitions );
+ final String[] values = values( definitions );
+
+ return StringUtils.replaceEach( text, keys, values );
+ }
+
+ private String[] keys( final Map<String, String> definitions ) {
+ final int size = definitions.size();
+ return definitions.keySet().toArray( new String[ size ] );
+ }
+
+ private String[] values( final Map<String, String> definitions ) {
+ final int size = definitions.size();
+ return definitions.values().toArray( new String[ size ] );
+ }
+
+ /**
+ * Decomposes a period of time into days, hours, minutes, seconds,
+ * milliseconds, and nanoseconds.
+ *
+ * @param duration Time in nanoseconds.
+ *
+ * @return A non-null, comma-separated string (without newline).
+ */
+ public String elapsed( long duration ) {
+ final TimeUnit scale = NANOSECONDS;
+
+ long days = scale.toDays( duration );
+ duration -= DAYS.toMillis( days );
+ long hours = scale.toHours( duration );
+ duration -= HOURS.toMillis( hours );
+ long minutes = scale.toMinutes( duration );
+ duration -= MINUTES.toMillis( minutes );
+ long seconds = scale.toSeconds( duration );
+ duration -= SECONDS.toMillis( seconds );
+ long millis = scale.toMillis( duration );
+ duration -= MILLISECONDS.toMillis( seconds );
+ long nanos = scale.toNanos( duration );
+
+ return String.format(
+ "%d days, %d hours, %d minutes, %d seconds, %d millis, %d nanos",
+ days, hours, minutes, seconds, millis, nanos
+ );
+ }
+
+ private void injectVariables( final Map<String, String> definitions ) {
+ for( int i = (SOURCE.length() / MATCHES_DIVISOR) + 1; i > 0; i-- ) {
+ final int r = current().nextInt( 1, SOURCE.length() );
+ SOURCE.insert( r, randomKey( definitions ) );
+ }
+ }
+
+ private String randomKey( final Map<String, String> map ) {
+ final Object[] keys = map.keySet().toArray();
+ final int r = current().nextInt( keys.length );
+ return keys[ r ].toString();
+ }
+
+ private void populate( final TreeItem<String> parent, final Map<String, String> map ) {
+ for( final TreeItem<String> child : parent.getChildren() ) {
+ if( child.isLeaf() ) {
+ final String key = asDefinition( ((VariableTreeItem<String>)child).toPath() );
+ final String value = child.getValue();
+
+ map.put( key, value );
+ } else {
+ populate( child, map );
+ }
+ }
+ }
+
+ private String asDefinition( final String key ) {
+ return "$" + key + "$";
+ }
+
+ public static void main( String[] args ) {
+ launch( args );
+ }
+}
src/main/java/com/scrivenvar/editor/MarkdownEditorPane.java
-/*
- * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.editor;
-
-import static com.scrivenvar.Constants.STYLESHEET_EDITOR;
-import com.scrivenvar.dialogs.ImageDialog;
-import com.scrivenvar.dialogs.LinkDialog;
-import com.scrivenvar.processors.MarkdownProcessor;
-import static com.scrivenvar.util.Utils.ltrim;
-import static com.scrivenvar.util.Utils.rtrim;
-import com.vladsch.flexmark.ast.Link;
-import com.vladsch.flexmark.ast.Node;
-import java.nio.file.Path;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javafx.beans.value.ObservableValue;
-import javafx.scene.control.Dialog;
-import javafx.scene.control.IndexRange;
-import static javafx.scene.input.KeyCode.ENTER;
-import javafx.scene.input.KeyEvent;
-import javafx.stage.Window;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-
-/**
- * Markdown editor pane.
- *
- * @author Karl Tauber and White Magic Software, Ltd.
- */
-public class MarkdownEditorPane extends EditorPane {
-
- private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile(
- "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
-
- public MarkdownEditorPane() {
- initEditor();
- }
-
- private void initEditor() {
- final StyleClassedTextArea textArea = getEditor();
-
- textArea.setWrapText( true );
- textArea.getStyleClass().add( "markdown-editor" );
- textArea.getStylesheets().add( STYLESHEET_EDITOR );
-
- addEventListener( keyPressed( ENTER ), this::enterPressed );
-
- // TODO: Wait for implementation that allows cutting lines, not paragraphs.
-// addEventListener( keyPressed( X, SHORTCUT_DOWN ), this::cutLine );
- }
-
- public ObservableValue<String> markdownProperty() {
- return getEditor().textProperty();
- }
-
- private void enterPressed( final KeyEvent e ) {
- final StyleClassedTextArea textArea = getEditor();
- final String currentLine = textArea.getText( textArea.getCurrentParagraph() );
- final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
-
- String newText = "\n";
-
- if( matcher.matches() ) {
- if( !matcher.group( 2 ).isEmpty() ) {
- // indent new line with same whitespace characters and list markers as current line
- newText = newText.concat( matcher.group( 1 ) );
- } else {
- // current line contains only whitespace characters and list markers
- // --> empty current line
- final int caretPosition = textArea.getCaretPosition();
- textArea.selectRange( caretPosition - currentLine.length(), caretPosition );
- }
- }
-
- textArea.replaceSelection( newText );
- }
-
- public void surroundSelection( final String leading, final String trailing ) {
- surroundSelection( leading, trailing, null );
- }
-
- public void surroundSelection( String leading, String trailing, final String hint ) {
- final StyleClassedTextArea textArea = getEditor();
-
- // Note: not using textArea.insertText() to insert leading and trailing
- // because this would add two changes to undo history
- final IndexRange selection = textArea.getSelection();
- int start = selection.getStart();
- int end = selection.getEnd();
-
- final String selectedText = textArea.getSelectedText();
-
- // remove leading and trailing whitespaces from selected text
- String trimmedSelectedText = selectedText.trim();
- if( trimmedSelectedText.length() < selectedText.length() ) {
- start += selectedText.indexOf( trimmedSelectedText );
- end = start + trimmedSelectedText.length();
- }
-
- // remove leading whitespaces from leading text if selection starts at zero
- if( start == 0 ) {
- leading = ltrim( leading );
- }
-
- // remove trailing whitespaces from trailing text if selection ends at text end
- if( end == textArea.getLength() ) {
- trailing = rtrim( trailing );
- }
-
- // remove leading line separators from leading text
- // if there are line separators before the selected text
- if( leading.startsWith( "\n" ) ) {
- for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
- if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
- break;
- }
- leading = leading.substring( 1 );
- }
- }
-
- // remove trailing line separators from trailing or leading text
- // if there are line separators after the selected text
- final boolean trailingIsEmpty = trailing.isEmpty();
- String str = trailingIsEmpty ? leading : trailing;
-
- if( str.endsWith( "\n" ) ) {
- final int length = textArea.getLength();
-
- for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
- if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
- break;
- }
-
- str = str.substring( 0, str.length() - 1 );
- }
-
- if( trailingIsEmpty ) {
- leading = str;
- } else {
- trailing = str;
- }
- }
-
- int selStart = start + leading.length();
- int selEnd = end + leading.length();
-
- // insert hint text if selection is empty
- if( hint != null && trimmedSelectedText.isEmpty() ) {
- trimmedSelectedText = hint;
- selEnd = selStart + hint.length();
- }
-
- // prevent undo merging with previous text entered by user
- getUndoManager().preventMerge();
-
- // replace text and update selection
- textArea.replaceText( start, end, leading + trimmedSelectedText + trailing );
- textArea.selectRange( selStart, selEnd );
- }
-
- /**
- * Returns one of: selected text, word under cursor, or parsed hyperlink from
- * the markdown AST.
- *
- * @return
- */
- private HyperlinkModel getHyperlink() {
- final StyleClassedTextArea textArea = getEditor();
- final String selectedText = textArea.getSelectedText();
-
- // Get the current paragraph, convert to Markdown nodes.
- final MarkdownProcessor mp = new MarkdownProcessor( null );
- final int p = textArea.getCurrentParagraph();
- final String paragraph = textArea.getText( p );
- final Node node = mp.toNode( paragraph );
- final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
- final Link link = visitor.process( node );
-
- if( link != null ) {
- textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
- }
-
- final HyperlinkModel model = createHyperlinkModel(
- link, selectedText, "https://website.com"
- );
-
- return model;
- }
-
- private HyperlinkModel createHyperlinkModel(
- final Link link, final String selection, final String url ) {
-
- return link == null
- ? new HyperlinkModel( selection, url )
- : new HyperlinkModel( link );
- }
-
- private Path getParentPath() {
- final Path parentPath = getPath();
- return (parentPath != null) ? parentPath.getParent() : null;
- }
-
- private Dialog<String> createLinkDialog() {
- return new LinkDialog( getWindow(), getHyperlink(), getParentPath() );
- }
-
- private Dialog<String> createImageDialog() {
- return new ImageDialog( getWindow(), getParentPath() );
- }
-
- private void insertObject( final Dialog<String> dialog ) {
- dialog.showAndWait().ifPresent( result -> {
- getEditor().replaceSelection( result );
- } );
- }
-
- public void insertLink() {
- insertObject( createLinkDialog() );
- }
-
- public void insertImage() {
- insertObject( createImageDialog() );
- }
-
- private Window getWindow() {
- return getScrollPane().getScene().getWindow();
- }
-}
src/main/java/com/scrivenvar/editor/VariableNameInjector.java
-/*
- * Copyright 2016 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.editor;
-
-import static com.scrivenvar.Constants.SEPARATOR;
-import com.scrivenvar.FileEditorTabPane;
-import com.scrivenvar.Services;
-import com.scrivenvar.decorators.VariableDecorator;
-import com.scrivenvar.decorators.YamlVariableDecorator;
-import com.scrivenvar.definition.DefinitionPane;
-import static com.scrivenvar.definition.Lists.getFirst;
-import static com.scrivenvar.definition.Lists.getLast;
-import com.scrivenvar.service.Settings;
-import com.scrivenvar.ui.VariableTreeItem;
-import static java.lang.Character.isSpaceChar;
-import static java.lang.Character.isWhitespace;
-import static java.lang.Math.min;
-import java.util.function.Consumer;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.scene.control.IndexRange;
-import javafx.scene.control.TreeItem;
-import javafx.scene.input.InputEvent;
-import javafx.scene.input.KeyCode;
-import static javafx.scene.input.KeyCode.AT;
-import static javafx.scene.input.KeyCode.DIGIT2;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCode.MINUS;
-import static javafx.scene.input.KeyCode.SPACE;
-import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
-import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
-import javafx.scene.input.KeyEvent;
-import org.fxmisc.richtext.StyledTextArea;
-import org.fxmisc.wellbehaved.event.EventPattern;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
-import org.fxmisc.wellbehaved.event.InputMap;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-import static org.fxmisc.wellbehaved.event.InputMap.sequence;
-
-/**
- * Provides the logic for injecting variable names within the editor.
- *
- * @author White Magic Software, Ltd.
- */
-public class VariableNameInjector {
-
- public static final int DEFAULT_MAX_VAR_LENGTH = 64;
-
- private static final int NO_DIFFERENCE = -1;
-
- private final Settings settings = Services.load( Settings.class );
-
- /**
- * Used to capture keyboard events once the user presses @.
- */
- private InputMap<InputEvent> keyboardMap;
-
- private FileEditorTabPane fileEditorPane;
- private DefinitionPane definitionPane;
-
- /**
- * Position of the variable in the text when in variable mode (0 by default).
- */
- private int initialCaretPosition;
-
- public VariableNameInjector(
- final FileEditorTabPane editorPane,
- final DefinitionPane definitionPane ) {
- setFileEditorPane( editorPane );
- setDefinitionPane( definitionPane );
-
- initKeyboardEventListeners();
- }
-
- /**
- * Traps keys for performing various short-cut tasks, such as @-mode variable
- * insertion and control+space for variable autocomplete.
- *
- * @ key is pressed, a new keyboard map is inserted in place of the current
- * map -- this class goes into "variable edit mode" (a.k.a. vMode).
- *
- * @see createKeyboardMap()
- */
- private void initKeyboardEventListeners() {
- addEventListener( keyPressed( SPACE, CONTROL_DOWN ), this::autocomplete );
-
- // @ key in Linux?
- addEventListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode );
- // @ key in Windows.
- addEventListener( keyPressed( AT ), this::vMode );
- }
-
- /**
- * The @ symbol is a short-cut to inserting a YAML variable reference.
- *
- * @param e Superfluous information about the key that was pressed.
- */
- private void vMode( KeyEvent e ) {
- setInitialCaretPosition();
- vModeStart();
- vModeAutocomplete();
- }
-
- /**
- * Receives key presses until the user completes the variable selection. This
- * allows the arrow keys to be used for selecting variables.
- *
- * @param e The key that was pressed.
- */
- private void vModeKeyPressed( KeyEvent e ) {
- final KeyCode keyCode = e.getCode();
-
- switch( keyCode ) {
- case BACK_SPACE:
- // Don't decorate the variable upon exiting vMode.
- vModeBackspace();
- break;
-
- case ESCAPE:
- // Don't decorate the variable upon exiting vMode.
- vModeStop();
- break;
-
- case ENTER:
- case PERIOD:
- case RIGHT:
- case END:
- // Stop at a leaf node, ENTER means accept.
- if( vModeConditionalComplete() && keyCode == ENTER ) {
- vModeStop();
-
- // Decorate the variable upon exiting vMode.
- decorateVariable();
- }
- break;
-
- case UP:
- cyclePathPrev();
- break;
-
- case DOWN:
- cyclePathNext();
- break;
-
- default:
- vModeFilterKeyPressed( e );
- break;
- }
-
- e.consume();
- }
-
- private void vModeBackspace() {
- deleteSelection();
-
- // Break out of variable mode by back spacing to the original position.
- if( getCurrentCaretPosition() > getInitialCaretPosition() ) {
- vModeAutocomplete();
- } else {
- vModeStop();
- }
- }
-
- /**
- * Updates the text with the path selected (or typed) by the user.
- */
- private void vModeAutocomplete() {
- final TreeItem<String> node = getCurrentNode();
-
- if( !node.isLeaf() ) {
- final String word = getLastPathWord();
- final String label = node.getValue();
- final int delta = difference( label, word );
- final String remainder = delta == NO_DIFFERENCE
- ? label
- : label.substring( delta );
-
- final StyledTextArea textArea = getEditor();
- final int posBegan = getCurrentCaretPosition();
- final int posEnded = posBegan + remainder.length();
-
- textArea.replaceSelection( remainder );
-
- if( posEnded - posBegan > 0 ) {
- textArea.selectRange( posEnded, posBegan );
- }
-
- expand( node );
- }
- }
-
- /**
- * Only variable name keys can pass through the filter. This is called when
- * the user presses a key.
- *
- * @param e The key that was pressed.
- */
- private void vModeFilterKeyPressed( final KeyEvent e ) {
- if( isVariableNameKey( e ) ) {
- typed( e.getText() );
- }
- }
-
- /**
- * Performs an autocomplete depending on whether the user has finished typing
- * in a word. If there is a selected range, then this will complete the most
- * recent word and jump to the next child.
- *
- * @return true The auto-completed node was a terminal node.
- */
- private boolean vModeConditionalComplete() {
- acceptPath();
-
- final TreeItem<String> node = getCurrentNode();
- final boolean terminal = isTerminal( node );
-
- if( !terminal ) {
- typed( SEPARATOR );
- }
-
- return terminal;
- }
-
- /**
- * Pressing control+space will find a node that matches the current word and
- * substitute the YAML variable reference. This is called when the user is not
- * editing in vMode.
- *
- * @param e Ignored -- it can only be Ctrl+Space.
- */
- private void autocomplete( KeyEvent e ) {
- final String paragraph = getCaretParagraph();
- final int[] boundaries = getWordBoundaries( paragraph );
- final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
-
- final VariableTreeItem<String> leaf = findLeaf( word );
-
- if( leaf != null ) {
- replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() );
- decorateVariable();
- expand( leaf );
- }
- }
-
- /**
- * Called when autocomplete finishes on a valid leaf or when the user presses
- * Enter to finish manual autocomplete.
- */
- private void decorateVariable() {
- // A little bit of duplication...
- final String paragraph = getCaretParagraph();
- final int[] boundaries = getWordBoundaries( paragraph );
- final String old = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
-
- final String newVariable = getVariableDecorator().decorate( old );
-
- final int posEnded = getCurrentCaretPosition();
- final int posBegan = posEnded - old.length();
-
- getEditor().replaceText( posBegan, posEnded, newVariable );
- }
-
- /**
- * Updates the text at the given position within the current paragraph.
- *
- * @param posBegan The starting index in the paragraph text to replace.
- * @param posEnded The ending index in the paragraph text to replace.
- * @param text Overwrite the paragraph substring with this text.
- */
- private void replaceText(
- final int posBegan, final int posEnded, final String text ) {
- final int p = getCurrentParagraph();
-
- getEditor().replaceText( p, posBegan, p, posEnded, text );
- }
-
- /**
- * Returns the caret's current paragraph position.
- *
- * @return A number greater than or equal to 0.
- */
- private int getCurrentParagraph() {
- return getEditor().getCurrentParagraph();
- }
-
- /**
- * Returns current word boundary indexes into the current paragraph, including
- * punctuation.
- *
- * @param p The paragraph wherein to hunt word boundaries.
- * @param offset The offset into the paragraph to begin scanning left and
- * right.
- *
- * @return The starting and ending index of the word closest to the caret.
- */
- private int[] getWordBoundaries( final String p, final int offset ) {
- // Remove dashes, but retain hyphens. Retain same number of characters
- // to preserve relative indexes.
- final String paragraph = p.replace( "---", " " ).replace( "--", " " );
-
- return getWordAt( paragraph, offset );
- }
-
- /**
- * Helper method to get the word boundaries for the current paragraph.
- *
- * @param paragraph
- *
- * @return
- */
- private int[] getWordBoundaries( final String paragraph ) {
- return getWordBoundaries( paragraph, getCurrentCaretColumn() );
- }
-
- /**
- * Given an arbitrary offset into a string, this returns the word at that
- * index. The inputs and outputs include:
- *
- * <ul>
- * <li>surrounded by space: <code>hello | world!</code> ("");</li>
- * <li>end of word: <code>hello| world!</code> ("hello");</li>
- * <li>start of a word: <code>hello |world!</code> ("world!");</li>
- * <li>within a word: <code>hello wo|rld!</code> ("world!");</li>
- * <li>end of a paragraph: <code>hello world!|</code> ("world!");</li>
- * <li>start of a paragraph: <code>|hello world!</code> ("hello!"); or</li>
- * <li>after punctuation: <code>hello world!|</code> ("world!").</li>
- * </ul>
- *
- * @param p The string to scan for a word.
- * @param offset The offset within s to begin searching for the nearest word
- * boundary, must not be out of bounds of s.
- *
- * @return The word in s at the offset.
- *
- * @see getWordBegan( String, int )
- * @see getWordEnded( String, int )
- */
- private int[] getWordAt( final String p, final int offset ) {
- return new int[]{ getWordBegan( p, offset ), getWordEnded( p, offset ) };
- }
-
- /**
- * Returns the index into s where a word begins.
- *
- * @param s Never null.
- * @param offset Index into s to begin searching backwards for a word
- * boundary.
- *
- * @return The index where a word begins.
- */
- private int getWordBegan( final String s, int offset ) {
- while( offset > 0 && isBoundary( s.charAt( offset - 1 ) ) ) {
- offset--;
- }
-
- return offset;
- }
-
- /**
- * Returns the index into s where a word ends.
- *
- * @param s Never null.
- * @param offset Index into s to begin searching forwards for a word boundary.
- *
- * @return The index where a word ends.
- */
- private int getWordEnded( final String s, int offset ) {
- final int length = s.length();
-
- while( offset < length && isBoundary( s.charAt( offset ) ) ) {
- offset++;
- }
-
- return offset;
- }
-
- /**
- * Returns true if the given character can be reasonably expected to be part
- * of a word, including punctuation marks.
- *
- * @param c The character to compare.
- *
- * @return false The character is a space character.
- */
- private boolean isBoundary( final char c ) {
- return !isSpaceChar( c );
- }
-
- /**
- * Returns the text for the paragraph that contains the caret.
- *
- * @return A non-null string, possibly empty.
- */
- private String getCaretParagraph() {
- return getEditor().getText( getCurrentParagraph() );
- }
-
- /**
- * Returns true if the node has children that can be selected (i.e., any
- * non-leaves).
- *
- * @param <T> The type that the TreeItem contains.
- * @param node The node to test for terminality.
- *
- * @return true The node has one branch and its a leaf.
- */
- private <T> boolean isTerminal( final TreeItem<T> node ) {
- final ObservableList<TreeItem<T>> branches = node.getChildren();
-
- return branches.size() == 1 && branches.get( 0 ).isLeaf();
- }
-
- /**
- * Inserts text that the user typed at the current caret position, then
- * performs an autocomplete for the variable name.
- *
- * @param text The text to insert, never null.
- */
- private void typed( final String text ) {
- getEditor().replaceSelection( text );
- vModeAutocomplete();
- }
-
- /**
- * Called when the user presses either End or Enter key.
- */
- private void acceptPath() {
- final IndexRange range = getSelectionRange();
-
- if( range != null ) {
- final int rangeEnd = range.getEnd();
- final StyledTextArea textArea = getEditor();
- textArea.deselect();
- textArea.moveTo( rangeEnd );
- }
- }
-
- /**
- * Replaces the entirety of the existing path (from the initial caret
- * position) with the given path.
- *
- * @param oldPath The path to replace.
- * @param newPath The replacement path.
- */
- private void replacePath( final String oldPath, final String newPath ) {
- final StyledTextArea textArea = getEditor();
- final int posBegan = getInitialCaretPosition();
- final int posEnded = posBegan + oldPath.length();
-
- textArea.deselect();
- textArea.replaceText( posBegan, posEnded, newPath );
- }
-
- /**
- * Called when the user presses the Backspace key.
- */
- private void deleteSelection() {
- final StyledTextArea textArea = getEditor();
- textArea.replaceSelection( "" );
- textArea.deletePreviousChar();
- }
-
- /**
- * Cycles the selected text through the nodes.
- *
- * @param direction true - next; false - previous
- */
- private void cycleSelection( final boolean direction ) {
- final TreeItem<String> node = getCurrentNode();
-
- // Find the sibling for the current selection and replace the current
- // selection with the sibling's value
- TreeItem< String> cycled = direction
- ? node.nextSibling()
- : node.previousSibling();
-
- // When cycling at the end (or beginning) of the list, jump to the first
- // (or last) sibling depending on the cycle direction.
- if( cycled == null ) {
- cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
- }
-
- final String path = getCurrentPath();
- final String cycledWord = cycled.getValue();
- final String word = getLastPathWord();
- final int index = path.indexOf( word );
- final String cycledPath = path.substring( 0, index ) + cycledWord;
-
- expand( cycled );
- replacePath( path, cycledPath );
- }
-
- /**
- * Cycles to the next sibling of the currently selected tree node.
- */
- private void cyclePathNext() {
- cycleSelection( true );
- }
-
- /**
- * Cycles to the previous sibling of the currently selected tree node.
- */
- private void cyclePathPrev() {
- cycleSelection( false );
- }
-
- /**
- * Returns the variable name (or as much as has been typed so far). Returns
- * all the characters from the initial caret column to the the first
- * whitespace character. This will return a path that contains zero or more
- * separators.
- *
- * @return A non-null string, possibly empty.
- */
- private String getCurrentPath() {
- final String s = extractTextChunk();
- final int length = s.length();
-
- int i = 0;
-
- while( i < length && !isWhitespace( s.charAt( i ) ) ) {
- i++;
- }
-
- return s.substring( 0, i );
- }
-
- private <T> ObservableList<TreeItem<T>> getSiblings(
- final TreeItem<T> item ) {
- final TreeItem<T> parent = item.getParent();
- return parent == null ? item.getChildren() : parent.getChildren();
- }
-
- private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
- return getFirst( getSiblings( item ), item );
- }
-
- private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
- return getLast( getSiblings( item ), item );
- }
-
- /**
- * Returns the caret position as an offset into the text.
- *
- * @return A value from 0 to the length of the text (minus one).
- */
- private int getCurrentCaretPosition() {
- return getEditor().getCaretPosition();
- }
-
- /**
- * Returns the caret position within the current paragraph.
- *
- * @return A value from 0 to the length of the current paragraph.
- */
- private int getCurrentCaretColumn() {
- return getEditor().getCaretColumn();
- }
-
- /**
- * Returns the last word from the path.
- *
- * @return The last token.
- */
- private String getLastPathWord() {
- String path = getCurrentPath();
-
- int i = path.indexOf( SEPARATOR );
-
- while( i > 0 ) {
- path = path.substring( i + 1 );
- i = path.indexOf( SEPARATOR );
- }
-
- return path;
- }
-
- /**
- * Returns text from the initial caret position until some arbitrarily long
- * number of characters. The number of characters extracted will be
- * getMaxVarLength, or fewer, depending on how many characters remain to be
- * extracted. The result from this method is trimmed to the first whitespace
- * character.
- *
- * @return A chunk of text that includes all the words representing a path,
- * and then some.
- */
- private String extractTextChunk() {
- final StyledTextArea textArea = getEditor();
- final int textBegan = getInitialCaretPosition();
- final int remaining = textArea.getLength() - textBegan;
- final int textEnded = min( remaining, getMaxVarLength() );
-
- return textArea.getText( textBegan, textEnded );
- }
-
- /**
- * Returns the node for the current path.
- */
- private TreeItem<String> getCurrentNode() {
- return findNode( getCurrentPath() );
- }
-
- /**
- * Finds the node that most closely matches the given path.
- *
- * @param path The path that represents a node.
- *
- * @return The node for the path, or the root node if the path could not be
- * found, but never null.
- */
- private TreeItem<String> findNode( final String path ) {
- return getDefinitionPane().findNode( path );
- }
-
- /**
- * Finds the first leaf having a value that starts with the given text.
- *
- * @param text The text to find in the definition tree.
- *
- * @return The leaf that starts with the given text, or null if not found.
- */
- private VariableTreeItem<String> findLeaf( final String text ) {
- return getDefinitionPane().findLeaf( text );
- }
-
- /**
- * Used to ignore typed keys in favour of trapping pressed keys.
- *
- * @param e The key that was typed.
- */
- private void vModeKeyTyped( KeyEvent e ) {
- e.consume();
- }
-
- /**
- * Used to lazily initialize the keyboard map.
- *
- * @return Mappings for keyTyped and keyPressed.
- */
- protected InputMap<InputEvent> createKeyboardMap() {
- return sequence(
- consume( keyTyped(), this::vModeKeyTyped ),
- consume( keyPressed(), this::vModeKeyPressed )
- );
- }
-
- private InputMap<InputEvent> getKeyboardMap() {
- if( this.keyboardMap == null ) {
- this.keyboardMap = createKeyboardMap();
- }
-
- return this.keyboardMap;
- }
-
- /**
- * Collapses the tree then expands and selects the given node.
- *
- * @param node The node to expand.
- */
- private void expand( final TreeItem<String> node ) {
- final DefinitionPane pane = getDefinitionPane();
- pane.collapse();
- pane.expand( node );
- pane.select( node );
- }
-
- /**
- * Returns true iff the key code the user typed can be used as part of a YAML
- * variable name.
- *
- * @param keyEvent Keyboard key press event information.
- *
- * @return true The key is a value that can be inserted into the text.
- */
- private boolean isVariableNameKey( final KeyEvent keyEvent ) {
- final KeyCode kc = keyEvent.getCode();
-
- return (kc.isLetterKey()
- || kc.isDigitKey()
- || (keyEvent.isShiftDown() && kc == MINUS))
- && !keyEvent.isControlDown();
- }
-
- /**
- * Starts to capture user input events.
- */
- private void vModeStart() {
- addEventListener( getKeyboardMap() );
- }
-
- /**
- * Restores capturing of user input events to the previous event listener.
- * Also asks the processing chain to modify the variable text into a
- * machine-readable variable based on the format required by the file type.
- * For example, a Markdown file (.md) will substitute a $VAR$ name while an R
- * file (.Rmd, .Rxml) will use `r#xVAR`.
- */
- private void vModeStop() {
- removeEventListener( getKeyboardMap() );
- }
-
- private VariableDecorator getVariableDecorator() {
- return new YamlVariableDecorator();
- }
-
- /**
- * Returns the index where the two strings diverge.
- *
- * @param s1 The string that could be a substring of s2, null allowed.
- * @param s2 The string that could be a substring of s1, null allowed.
- *
- * @return NO_DIFFERENCE if the strings are the same, otherwise the index
- * where they differ.
- */
- @SuppressWarnings( "StringEquality" )
- private int difference( final CharSequence s1, final CharSequence s2 ) {
- if( s1 == s2 ) {
- return NO_DIFFERENCE;
- }
-
- if( s1 == null || s2 == null ) {
- return 0;
- }
-
- int i = 0;
- final int limit = min( s1.length(), s2.length() );
-
- while( i < limit && s1.charAt( i ) == s2.charAt( i ) ) {
- i++;
- }
-
- // If one string was shorter than the other, that's where they differ.
- return i;
- }
-
- /**
- * Delegates to the file editor pane, and, ultimately, to its text area.
- */
- private <T extends Event, U extends T> void addEventListener(
- final EventPattern<? super T, ? extends U> event,
- final Consumer<? super U> consumer ) {
- getFileEditorPane().addEventListener( event, consumer );
- }
-
- /**
- * Delegates to the file editor pane, and, ultimately, to its text area.
- *
- * @param map The map of methods to events.
- */
- private void addEventListener( final InputMap<InputEvent> map ) {
- getFileEditorPane().addEventListener( map );
- }
-
- private void removeEventListener( final InputMap<InputEvent> map ) {
- getFileEditorPane().removeEventListener( map );
- }
-
- /**
- * Returns the position of the caret when variable mode editing was requested.
- *
- * @return The variable mode caret position.
- */
- private int getInitialCaretPosition() {
- return this.initialCaretPosition;
- }
-
- /**
- * Sets the position of the caret when variable mode editing was requested.
- * Stores the current position because only the text that comes afterwards is
- * a suitable variable reference.
- *
- * @return The variable mode caret position.
- */
- private void setInitialCaretPosition() {
- this.initialCaretPosition = getEditor().getCaretPosition();
- }
-
- private StyledTextArea getEditor() {
- return getFileEditorPane().getEditor();
- }
-
- public FileEditorTabPane getFileEditorPane() {
- return this.fileEditorPane;
- }
-
- private void setFileEditorPane( final FileEditorTabPane fileEditorPane ) {
- this.fileEditorPane = fileEditorPane;
- }
-
- private DefinitionPane getDefinitionPane() {
- return this.definitionPane;
- }
-
- private void setDefinitionPane( final DefinitionPane definitionPane ) {
- this.definitionPane = definitionPane;
- }
-
- private IndexRange getSelectionRange() {
- return getEditor().getSelection();
- }
-
- /**
- * Don't look ahead too far when trying to find the end of a node.
- *
- * @return 512 by default.
- */
- private int getMaxVarLength() {
- return getSettings().getSetting(
- "editor.variable.maxLength", DEFAULT_MAX_VAR_LENGTH );
- }
-
- private Settings getSettings() {
- return this.settings;
- }
-}
src/main/java/com/scrivenvar/definition/Lists.java
-/*
- * Copyright 2016 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import java.util.List;
-
-/**
- * Convenience class that provides a clearer API for obtaining list elements.
- *
- * @author White Magic Software, Ltd.
- */
-public final class Lists {
-
- private Lists() {
- }
-
- /**
- * Returns the first item in the given list, or null if not found.
- *
- * @param <T> The generic list type.
- * @param list The list that may have a first item.
- *
- * @return null if the list is null or there is no first item.
- */
- public static <T> T getFirst( final List<T> list ) {
- return getFirst( list, null );
- }
-
- /**
- * Returns the last item in the given list, or null if not found.
- *
- * @param <T> The generic list type.
- * @param list The list that may have a last item.
- *
- * @return null if the list is null or there is no last item.
- */
- public static <T> T getLast( final List<T> list ) {
- return getLast( list, null );
- }
-
- /**
- * Returns the first item in the given list, or t if not found.
- *
- * @param <T> The generic list type.
- * @param list The list that may have a first item.
- * @param t The default return value.
- *
- * @return null if the list is null or there is no first item.
- */
- public static <T> T getFirst( final List<T> list, final T t ) {
- return isEmpty( list ) ? t : list.get( 0 );
- }
-
- /**
- * Returns the last item in the given list, or t if not found.
- *
- * @param <T> The generic list type.
- * @param list The list that may have a last item.
- * @param t The default return value.
- *
- * @return null if the list is null or there is no last item.
- */
- public static <T> T getLast( final List<T> list, final T t ) {
- return isEmpty( list ) ? t : list.get( list.size() - 1 );
- }
-
- /**
- * Returns true if the given list is null or empty.
- *
- * @param <T> The generic list type.
- * @param list The list that has a last item.
- *
- * @return true The list is empty.
- */
- public static <T> boolean isEmpty( final List<T> list ) {
- return list == null || list.isEmpty();
- }
-}
src/main/java/com/scrivenvar/definition/VariableTreeItem.java
+/*
+ * Copyright 2016 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.definition;
+
+import com.scrivenvar.decorators.VariableDecorator;
+import com.scrivenvar.decorators.YamlVariableDecorator;
+import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
+import static com.scrivenvar.editors.VariableNameInjector.DEFAULT_MAX_VAR_LENGTH;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Stack;
+import javafx.scene.control.TreeItem;
+
+/**
+ * Provides behaviour afforded to variable names and their corresponding value.
+ *
+ * @author White Magic Software, Ltd.
+ * @param <T> The type of TreeItem (usually String).
+ */
+public class VariableTreeItem<T> extends TreeItem<T> {
+
+ private final static int DEFAULT_MAP_SIZE = 1000;
+
+ private final static VariableDecorator VARIABLE_DECORATOR =
+ new YamlVariableDecorator();
+
+ /**
+ * Flattened tree.
+ */
+ private Map<String, String> map;
+
+ /**
+ * Constructs a new item with a default value.
+ *
+ * @param value Passed up to superclass.
+ */
+ public VariableTreeItem( final T value ) {
+ super( value );
+ }
+
+ /**
+ * Finds a leaf starting at the current node with text that matches the given
+ * value.
+ *
+ * @param text The text to match against each leaf in the tree.
+ *
+ * @return The leaf that has a value starting with the given text.
+ */
+ public VariableTreeItem<T> findLeaf( final String text ) {
+ final Stack<VariableTreeItem<T>> stack = new Stack<>();
+ final VariableTreeItem<T> root = this;
+
+ stack.push( root );
+
+ boolean found = false;
+ VariableTreeItem<T> node = null;
+
+ while( !found && !stack.isEmpty() ) {
+ node = stack.pop();
+
+ if( node.valueStartsWith( text ) ) {
+ found = true;
+ } else {
+ for( final TreeItem<T> child : node.getChildren() ) {
+ stack.push( (VariableTreeItem<T>)child );
+ }
+
+ // No match found, yet.
+ node = null;
+ }
+ }
+
+ return (VariableTreeItem<T>)node;
+ }
+
+ /**
+ * Returns true if this node is a leaf and its value starts with the given
+ * text.
+ *
+ * @param s The text to compare against the node value.
+ *
+ * @return true Node is a leaf and its value starts with the given value.
+ */
+ private boolean valueStartsWith( final String s ) {
+ return isLeaf() && getValue().toString().startsWith( s );
+ }
+
+ /**
+ * Returns the path for this node, with nodes made distinct using the
+ * separator character. This uses two loops: one for pushing nodes onto a
+ * stack and one for popping them off to create the path in desired order.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ public String toPath() {
+ final Stack<TreeItem<T>> stack = new Stack<>();
+ TreeItem<T> node = this;
+
+ while( node.getParent() != null ) {
+ stack.push( node );
+ node = node.getParent();
+ }
+
+ final StringBuilder sb = new StringBuilder( DEFAULT_MAX_VAR_LENGTH );
+
+ while( !stack.isEmpty() ) {
+ node = stack.pop();
+
+ if( !node.isLeaf() ) {
+ sb.append( node.getValue() );
+
+ // This will add a superfluous separator, but instead of peeking at
+ // the stack all the time, the last separator will be removed outside
+ // the loop (one operation executed once).
+ sb.append( SEPARATOR );
+ }
+ }
+
+ // Remove the trailing SEPARATOR.
+ if( sb.length() > 0 ) {
+ sb.setLength( sb.length() - 1 );
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the hierarchy, flattened to key-value pairs.
+ *
+ * @return A map of this tree's key-value pairs.
+ */
+ public Map<String, String> getMap() {
+ if( this.map == null ) {
+ this.map = new HashMap<>( DEFAULT_MAP_SIZE );
+ populate( this, this.map );
+ }
+
+ return this.map;
+ }
+
+ private void populate( final TreeItem<T> parent, final Map<String, String> map ) {
+ for( final TreeItem<T> child : parent.getChildren() ) {
+ if( child.isLeaf() ) {
+ @SuppressWarnings( "unchecked" )
+ final String key = toVariable( ((VariableTreeItem<String>)child).toPath() );
+ final String value = child.getValue().toString();
+
+ map.put( key, value );
+ } else {
+ populate( child, map );
+ }
+ }
+ }
+
+ /**
+ * Converts the name of the key to a simple variable by enclosing it with
+ * dollar symbols.
+ *
+ * @param key The key name to change to a variable.
+ *
+ * @return $key$
+ */
+ public String toVariable( final String key ) {
+ return VARIABLE_DECORATOR.decorate( key );
+ }
+}
src/main/java/com/scrivenvar/TestDefinitionPane.java
-/*
- * Copyright 2016 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.scrivenvar.definition.DefinitionPane;
-import static javafx.application.Application.launch;
-import javafx.scene.control.TreeItem;
-import javafx.scene.control.TreeView;
-import javafx.stage.Stage;
-
-/**
- * TestDefinitionPane application for debugging.
- */
-public final class TestDefinitionPane extends TestHarness {
- /**
- * Application entry point.
- *
- * @param stage The primary application stage.
- *
- * @throws Exception Could not read configuration file.
- */
- @Override
- public void start( final Stage stage ) throws Exception {
- super.start( stage );
-
- TreeView<String> root = createTreeView();
- DefinitionPane pane = createDefinitionPane( root );
-
- test( pane, "language.ai.", "article" );
- test( pane, "language.ai", "ai" );
- test( pane, "l", "location" );
- test( pane, "la", "language" );
- test( pane, "c.p.n", "name" );
- test( pane, "c.p.n.", "First" );
- test( pane, "...", "c" );
- test( pane, "foo", "c" );
- test( pane, "foo.bar", "c" );
- test( pane, "", "c" );
- test( pane, "c", "protagonist" );
- test( pane, "c.", "protagonist" );
- test( pane, "c.p", "protagonist" );
- test( pane, "c.protagonist", "protagonist" );
-
- System.exit( 0 );
- }
-
- private void test( DefinitionPane pane, String path, String value ) {
- System.out.println( "---------------------------" );
- System.out.println( "Find Path: '" + path + "'" );
- final TreeItem<String> node = pane.findNode( path );
- System.out.println( "Path Node: " + node );
- System.out.println( "Node Val : " + node.getValue() );
- }
-
- public static void main( String[] args ) {
- launch( args );
- }
-}
src/main/java/com/scrivenvar/TestHarness.java
-/*
- * Copyright 2016 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import static com.scrivenvar.Messages.get;
-import com.scrivenvar.definition.DefinitionPane;
-import com.scrivenvar.yaml.YamlParser;
-import com.scrivenvar.yaml.YamlTreeAdapter;
-import java.io.IOException;
-import java.io.InputStream;
-import javafx.application.Application;
-import javafx.scene.Scene;
-import javafx.scene.control.TreeView;
-import javafx.scene.layout.BorderPane;
-import javafx.stage.Stage;
-import org.fxmisc.flowless.VirtualizedScrollPane;
-import org.fxmisc.richtext.StyleClassedTextArea;
-
-/**
- * TestDefinitionPane application for debugging and head-banging.
- */
-public abstract class TestHarness extends Application {
-
- private static Application app;
- private Scene scene;
-
- /**
- * Application entry point.
- *
- * @param stage The primary application stage.
- *
- * @throws Exception Could not read configuration file.
- */
- @Override
- public void start( final Stage stage ) throws Exception {
- initApplication();
- initScene();
- initStage( stage );
- }
-
- protected TreeView<String> createTreeView() throws IOException {
- return new YamlTreeAdapter( new YamlParser() ).adapt(
- asStream( "/com/scrivenvar/variables.yaml" ),
- get( "Pane.defintion.node.root.title" )
- );
- }
-
- protected DefinitionPane createDefinitionPane( TreeView<String> root ) {
- return new DefinitionPane( root );
- }
-
- private void initApplication() {
- app = this;
- }
-
- private void initScene() {
- final StyleClassedTextArea editor = new StyleClassedTextArea( false );
- final VirtualizedScrollPane<StyleClassedTextArea> scrollPane = new VirtualizedScrollPane<>( editor );
-
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1024, 800 );
- borderPane.setCenter( scrollPane );
-
- setScene( new Scene( borderPane ) );
- }
-
- private void initStage( Stage stage ) {
- stage.setScene( getScene() );
- }
-
- private Scene getScene() {
- return this.scene;
- }
-
- private void setScene( Scene scene ) {
- this.scene = scene;
- }
-
- private static Application getApplication() {
- return app;
- }
-
- public static void showDocument( String uri ) {
- getApplication().getHostServices().showDocument( uri );
- }
-
- protected InputStream asStream( String resource ) {
- return getClass().getResourceAsStream( resource );
- }
-}
src/main/java/com/scrivenvar/TestVariableNameProcessor.java
-/*
- * Copyright 2016 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.scrivenvar.ui.VariableTreeItem;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import static java.util.concurrent.ThreadLocalRandom.current;
-import java.util.concurrent.TimeUnit;
-import static java.util.concurrent.TimeUnit.DAYS;
-import static java.util.concurrent.TimeUnit.HOURS;
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.NANOSECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static javafx.application.Application.launch;
-import javafx.scene.control.TreeItem;
-import javafx.scene.control.TreeView;
-import javafx.stage.Stage;
-import org.ahocorasick.trie.*;
-import org.ahocorasick.trie.Trie.TrieBuilder;
-import static org.apache.commons.lang.RandomStringUtils.randomNumeric;
-import org.apache.commons.lang.StringUtils;
-
-/**
- * Tests substituting variable definitions with their values in a swath of text.
- *
- * @author White Magic Software, Ltd.
- */
-public class TestVariableNameProcessor extends TestHarness {
-
- private final static int TEXT_SIZE = 1000000;
- private final static int MATCHES_DIVISOR = 1000;
-
- private final static StringBuilder SOURCE
- = new StringBuilder( randomNumeric( TEXT_SIZE ) );
-
- private final static boolean DEBUG = false;
-
- public TestVariableNameProcessor() {
- }
-
- @Override
- public void start( final Stage stage ) throws Exception {
- super.start( stage );
-
- final TreeView<String> treeView = createTreeView();
- final Map<String, String> definitions = new HashMap<>();
-
- populate( treeView.getRoot(), definitions );
- injectVariables( definitions );
-
- final String text = SOURCE.toString();
-
- show( text );
-
- long duration = System.nanoTime();
-
- // TODO: Test replaceEach (with intercoluated variables) and replaceEachRepeatedly
- // (without intercoluation).
- final String result = testBorAhoCorasick( text, definitions );
-
- duration = System.nanoTime() - duration;
-
- show( result );
- System.out.println( elapsed( duration ) );
-
- System.exit( 0 );
- }
-
- private void show( final String s ) {
- if( DEBUG ) {
- System.out.printf( "%s\n\n", s );
- }
- }
-
- private String testBorAhoCorasick(
- final String text,
- final Map<String, String> definitions ) {
- // Create a buffer sufficiently large that re-allocations are minimized.
- final StringBuilder sb = new StringBuilder( text.length() << 1 );
-
- final TrieBuilder builder = Trie.builder();
- builder.onlyWholeWords();
- builder.removeOverlaps();
-
- final String[] keys = keys( definitions );
-
- for( final String key : keys ) {
- builder.addKeyword( key );
- }
-
- final Trie trie = builder.build();
- final Collection<Emit> emits = trie.parseText( text );
-
- int prevIndex = 0;
-
- for( final Emit emit : emits ) {
- final int matchIndex = emit.getStart();
-
- sb.append( text.substring( prevIndex, matchIndex ) );
- sb.append( definitions.get( emit.getKeyword() ) );
- prevIndex = emit.getEnd() + 1;
- }
-
- // Add the remainder of the string (contains no more matches).
- sb.append( text.substring( prevIndex ) );
-
- return sb.toString();
- }
-
- private String testStringUtils(
- final String text, final Map<String, String> definitions ) {
- final String[] keys = keys( definitions );
- final String[] values = values( definitions );
-
- return StringUtils.replaceEach( text, keys, values );
- }
-
- private String[] keys( final Map<String, String> definitions ) {
- final int size = definitions.size();
- return definitions.keySet().toArray( new String[ size ] );
- }
-
- private String[] values( final Map<String, String> definitions ) {
- final int size = definitions.size();
- return definitions.values().toArray( new String[ size ] );
- }
-
- /**
- * Decomposes a period of time into days, hours, minutes, seconds,
- * milliseconds, and nanoseconds.
- *
- * @param duration Time in nanoseconds.
- *
- * @return A non-null, comma-separated string (without newline).
- */
- public String elapsed( long duration ) {
- final TimeUnit scale = NANOSECONDS;
-
- long days = scale.toDays( duration );
- duration -= DAYS.toMillis( days );
- long hours = scale.toHours( duration );
- duration -= HOURS.toMillis( hours );
- long minutes = scale.toMinutes( duration );
- duration -= MINUTES.toMillis( minutes );
- long seconds = scale.toSeconds( duration );
- duration -= SECONDS.toMillis( seconds );
- long millis = scale.toMillis( duration );
- duration -= MILLISECONDS.toMillis( seconds );
- long nanos = scale.toNanos( duration );
-
- return String.format(
- "%d days, %d hours, %d minutes, %d seconds, %d millis, %d nanos",
- days, hours, minutes, seconds, millis, nanos
- );
- }
-
- private void injectVariables( final Map<String, String> definitions ) {
- for( int i = (SOURCE.length() / MATCHES_DIVISOR) + 1; i > 0; i-- ) {
- final int r = current().nextInt( 1, SOURCE.length() );
- SOURCE.insert( r, randomKey( definitions ) );
- }
- }
-
- private String randomKey( final Map<String, String> map ) {
- final Object[] keys = map.keySet().toArray();
- final int r = current().nextInt( keys.length );
- return keys[ r ].toString();
- }
-
- private void populate( final TreeItem<String> parent, final Map<String, String> map ) {
- for( final TreeItem<String> child : parent.getChildren() ) {
- if( child.isLeaf() ) {
- final String key = asDefinition( ((VariableTreeItem<String>)child).toPath() );
- final String value = child.getValue();
-
- map.put( key, value );
- } else {
- populate( child, map );
- }
- }
- }
-
- private String asDefinition( final String key ) {
- return "$" + key + "$";
- }
-
- public static void main( String[] args ) {
- launch( args );
- }
-}