| Author | djarvis <email> |
|---|---|
| Date | 2016-12-13 20:40:44 GMT-0800 |
| Commit | 5a3ec32754596b8706bc5890a4dba5a8ea85b58b |
| Parent | 912fad1 |
| Delta | 465 lines added, 1750 lines removed, 1285-line decrease |
| # ######################################################################## | ||
| # | ||
| +# 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=*.* | ||
| # ######################################################################## |
| -/* | ||
| - * 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; | ||
| - } | ||
| - | ||
| -} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||