Dave Jarvis' Repositories

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

Reorganized build file. Added wrap to find. Persisted find text for subsequent find requests. Added find next ability. Updated build scripts to create ICO file from logo.

Authordjarvis <email>
Date2017-01-15 21:25:13 GMT-0800
Commit9c126871c50557357f7786a3cd7e57cc4a2d32f0
Parentae80ea0
build.gradle
-version = '1.1.0'
-
apply plugin: 'java'
apply plugin: 'java-library-distribution'
apply plugin: 'application'
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-
-applicationName = 'scrivenvar'
-
-mainClassName = 'com.scrivenvar.Main'
repositories {
compile files('libs/renjin-script-engine-0.8.2309-jar-with-dependencies.jar')
}
+
+version = '1.1.1'
+applicationName = 'scrivenvar'
+mainClassName = 'com.scrivenvar.Main'
+sourceCompatibility = JavaVersion.VERSION_1_8
jar {
src/main/java/com/scrivenvar/FileEditorTab.java
public void searchNext( final String needle ) {
final String haystack = getEditorText();
- final int index = haystack.indexOf( needle, getCaretPosition() );
+ int index = haystack.indexOf( needle, getCaretPosition() );
+
+ // Wrap around.
+ if( index == -1 ) {
+ index = haystack.indexOf( needle, 0 );
+ }
if( index >= 0 ) {
src/main/java/com/scrivenvar/MainWindow.java
private StatusBar statusBar;
private Text lineNumberText;
-
- private DefinitionSource definitionSource;
- private DefinitionPane definitionPane;
- private FileEditorTabPane fileEditorPane;
- private HTMLPreviewPane previewPane;
-
- /**
- * Prevent re-instantiation processing classes.
- */
- private Map<FileEditorTab, Processor<String>> processors;
-
- public MainWindow() {
- initLayout();
- initSnitch();
- initDefinitionListener();
- initTabAddedListener();
- initTabChangedListener();
- initPreferences();
- }
-
- /**
- * Listen for file editor tab pane to receive an open definition source event.
- */
- private void initDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- (ObservableValue<? extends Path> definitionFile,
- final Path oldPath, final Path newPath) -> {
- openDefinition( newPath );
-
- // Indirectly refresh the resolved map.
- setProcessors( null );
-
- updateDefinitionPane();
-
- try {
- getSnitch().ignore( oldPath );
- getSnitch().listen( newPath );
- } catch( final IOException ex ) {
- error( ex );
- }
-
- // Will create new processors and therefore a new resolved map.
- refreshSelectedTab( getActiveFileEditor() );
- }
- );
- }
-
- /**
- * When tabs are added, hook the various change listeners onto the new tab so
- * that the preview pane refreshes as necessary.
- */
- private void initTabAddedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Make sure the text processor kicks off when new files are opened.
- final ObservableList<Tab> tabs = editorPane.getTabs();
-
- // Update the preview pane on tab changes.
- tabs.addListener(
- (final Change<? extends Tab> change) -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- // Multiple tabs can be added simultaneously.
- for( final Tab newTab : change.getAddedSubList() ) {
- final FileEditorTab tab = (FileEditorTab)newTab;
-
- initTextChangeListener( tab );
- initCaretParagraphListener( tab );
- initVariableNameInjector( tab );
-// initSyntaxListener( tab );
- }
- }
- }
- }
- );
- }
-
- /**
- * Reloads the preferences from the previous load.
- */
- private void initPreferences() {
- restoreDefinitionSource();
- getFileEditorPane().restorePreferences();
- updateDefinitionPane();
- }
-
- /**
- * Listen for new tab selection events.
- */
- private void initTabChangedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabSelectionListener(
- (ObservableValue<? extends Tab> tabPane,
- final Tab oldTab, final Tab newTab) -> {
-
- // If there was no old tab, then this is a first time load, which
- // can be ignored.
- if( oldTab != null ) {
- if( newTab == null ) {
- closeRemainingTab();
- }
- else {
- // Update the preview with the edited text.
- refreshSelectedTab( (FileEditorTab)newTab );
- }
- }
- }
- );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener(
- (ObservableValue<? extends String> editor,
- final String oldValue, final String newValue) -> {
- refreshSelectedTab( tab );
- }
- );
- }
-
- private void initCaretParagraphListener( final FileEditorTab tab ) {
- tab.addCaretParagraphListener(
- (ObservableValue<? extends Integer> editor,
- final Integer oldValue, final Integer newValue) -> {
- refreshSelectedTab( tab );
- }
- );
- }
-
- private void initVariableNameInjector( final FileEditorTab tab ) {
- VariableNameInjector.listen( tab, getDefinitionPane() );
- }
-
- /**
- * Watch for changes to external files. In particular, this awaits
- * modifications to any XSL files associated with XML files being edited. When
- * an XSL file is modified (external to the application), the snitch's ears
- * perk up and the file is reloaded. This keeps the XSL transformation up to
- * date with what's on the file system.
- */
- private void initSnitch() {
- getSnitch().addObserver( this );
- }
-
- /**
- * Called whenever the preview pane becomes out of sync with the file editor
- * tab. This can be called when the text changes, the caret paragraph changes,
- * or the file tab changes.
- *
- * @param tab The file editor tab that has been changed in some fashion.
- */
- private void refreshSelectedTab( final FileEditorTab tab ) {
- if( tab.isFileOpen() ) {
- getPreviewPane().setPath( tab.getPath() );
-
- // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
- final Position p = tab.getCaretOffset();
- getLineNumberText().setText(
- get( STATUS_BAR_LINE,
- p.getMajor() + 1,
- p.getMinor() + 1,
- tab.getCaretPosition() + 1
- )
- );
-
- Processor<String> processor = getProcessors().get( tab );
-
- if( processor == null ) {
- processor = createProcessor( tab );
- getProcessors().put( tab, processor );
- }
-
- try {
- getNotifier().clear();
- processor.processChain( tab.getEditorText() );
- } catch( final Exception ex ) {
- error( ex );
- }
- }
- }
-
- /**
- * Used to find text in the active file editor window.
- */
- private void find() {
- final TextField input = new TextField();
-
- input.setOnKeyPressed( (KeyEvent event) -> {
- switch( event.getCode() ) {
- case F3:
- case ENTER:
- getActiveFileEditor().searchNext( input.getText() );
- break;
- case F:
- if( !event.isControlDown() ) {
- break;
- }
- case ESCAPE:
- getStatusBar().setGraphic( null );
- getActiveFileEditor().getEditorPane().requestFocus();
- break;
- }
- } );
-
- // Remove when the input field loses focus.
- input.focusedProperty().addListener(
- (
- final ObservableValue<? extends Boolean> focused,
- final Boolean oFocus,
- final Boolean nFocus) -> {
- if( !nFocus ) {
- getStatusBar().setGraphic( null );
- }
- }
- );
-
- getStatusBar().setGraphic( input );
-
- input.requestFocus();
- }
-
- public void findNext() {
- System.out.println( "find next" );
- }
-
- /**
- * Returns the variable map of interpolated definitions.
- *
- * @return A map to help dereference variables.
- */
- private Map<String, String> getResolvedMap() {
- return getDefinitionSource().getResolvedMap();
- }
-
- /**
- * Returns the root node for the hierarchical definition source.
- *
- * @return Data to display in the definition pane.
- */
- private TreeView<String> getTreeView() {
- try {
- return getDefinitionSource().asTreeView();
- } catch( Exception e ) {
- error( e );
- }
-
- // Slightly redundant as getDefinitionSource() might have returned an
- // empty definition source.
- return (new EmptyDefinitionSource()).asTreeView();
- }
-
- /**
- * Called when a definition source is opened.
- *
- * @param path Path to the definition source that was opened.
- */
- private void openDefinition( final Path path ) {
- try {
- final DefinitionSource ds = createDefinitionSource( path.toString() );
- setDefinitionSource( ds );
- storeDefinitionSource();
- updateDefinitionPane();
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private void updateDefinitionPane() {
- getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
- }
-
- private void restoreDefinitionSource() {
- final Preferences preferences = getPreferences();
- final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
-
- // If there's no definition source set, don't try to load it.
- if( source != null ) {
- setDefinitionSource( createDefinitionSource( source ) );
- }
- }
-
- private void storeDefinitionSource() {
- final Preferences preferences = getPreferences();
- final DefinitionSource ds = getDefinitionSource();
-
- preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
- }
-
- /**
- * Called when the last open tab is closed to clear the preview pane.
- */
- private void closeRemainingTab() {
- getPreviewPane().clear();
- }
-
- /**
- * Called when an exception occurs that warrants the user's attention.
- *
- * @param e The exception with a message that the user should know about.
- */
- private void error( final Exception e ) {
- getNotifier().notify( e );
- }
-
- //---- File actions -------------------------------------------------------
- /**
- * Called when an observable instance has changed. This is called by both the
- * snitch service and the notify service. The snitch service can be called for
- * different file types, including definition sources.
- *
- * @param observable The observed instance.
- * @param value The noteworthy item.
- */
- @Override
- public void update( final Observable observable, final Object value ) {
- if( value != null ) {
- if( observable instanceof Snitch && value instanceof Path ) {
- final Path path = (Path)value;
- final FileTypePredicate predicate
- = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
-
- // Reload definitions.
- if( predicate.test( path.toFile() ) ) {
- updateDefinitionSource( path );
- }
-
- updateSelectedTab();
- }
- else if( observable instanceof Notifier && value instanceof String ) {
- updateStatusBar( (String)value );
- }
- }
- }
-
- /**
- * Updates the status bar to show the given message.
- *
- * @param s The message to show in the status bar.
- */
- private void updateStatusBar( final String s ) {
- Platform.runLater(
- () -> {
- final int index = s.indexOf( '\n' );
- final String message = s.substring( 0, index > 0 ? index : s.length() );
-
- getStatusBar().setText( message );
- }
- );
- }
-
- /**
- * Called when a file has been modified.
- *
- * @param file Path to the modified file.
- */
- private void updateSelectedTab() {
- Platform.runLater(
- () -> {
- // Brute-force XSLT file reload by re-instantiating all processors.
- resetProcessors();
- refreshSelectedTab( getActiveFileEditor() );
- }
- );
- }
-
- /**
- * Reloads the definition source from the given path.
- *
- * @param path The path containing new definition information.
- */
- private void updateDefinitionSource( final Path path ) {
- Platform.runLater(
- () -> {
- openDefinition( path );
- }
- );
- }
-
- /**
- * After resetting the processors, they will refresh anew to be up-to-date
- * with the files (text and definition) currently loaded into the editor.
- */
- private void resetProcessors() {
- getProcessors().clear();
- }
-
- //---- File actions -------------------------------------------------------
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditor(), true );
- }
-
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditor() );
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Help actions -------------------------------------------------------
- private void helpAbout() {
- Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( get( "Dialog.about.title" ) );
- alert.setHeaderText( get( "Dialog.about.header" ) );
- alert.setContentText( get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- //---- Convenience accessors ----------------------------------------------
- private float getFloat( final String key, final float defaultValue ) {
- return getPreferences().getFloat( key, defaultValue );
- }
-
- private Preferences getPreferences() {
- return getOptions().getState();
- }
-
- protected Scene getScene() {
- if( this.scene == null ) {
- this.scene = createScene();
- }
-
- return this.scene;
- }
-
- public Window getWindow() {
- return getScene().getWindow();
- }
-
- private MarkdownEditorPane getActiveEditor() {
- final EditorPane pane = getActiveFileEditor().getEditorPane();
-
- return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
- }
-
- private FileEditorTab getActiveFileEditor() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- //---- Member accessors ---------------------------------------------------
- private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
- this.processors = map;
- }
-
- private Map<FileEditorTab, Processor<String>> getProcessors() {
- if( this.processors == null ) {
- setProcessors( new HashMap<>() );
- }
-
- return this.processors;
- }
-
- private FileEditorTabPane getFileEditorPane() {
- if( this.fileEditorPane == null ) {
- this.fileEditorPane = createFileEditorPane();
- }
-
- return this.fileEditorPane;
- }
-
- private HTMLPreviewPane getPreviewPane() {
- if( this.previewPane == null ) {
- this.previewPane = createPreviewPane();
- }
-
- return this.previewPane;
- }
-
- private void setDefinitionSource( final DefinitionSource definitionSource ) {
- this.definitionSource = definitionSource;
- }
-
- private DefinitionSource getDefinitionSource() {
- if( this.definitionSource == null ) {
- this.definitionSource = new EmptyDefinitionSource();
- }
-
- return this.definitionSource;
- }
-
- private DefinitionPane getDefinitionPane() {
- if( this.definitionPane == null ) {
- this.definitionPane = createDefinitionPane();
- }
-
- return this.definitionPane;
- }
-
- private Options getOptions() {
- return this.options;
- }
-
- private Snitch getSnitch() {
- return this.snitch;
- }
-
- private Notifier getNotifier() {
- return this.notifier;
- }
-
- public void setMenuBar( final MenuBar menuBar ) {
- this.menuBar = menuBar;
- }
-
- public MenuBar getMenuBar() {
- return this.menuBar;
- }
-
- private Text getLineNumberText() {
- if( this.lineNumberText == null ) {
- this.lineNumberText = createLineNumberText();
- }
-
- return this.lineNumberText;
- }
-
- private synchronized StatusBar getStatusBar() {
- if( this.statusBar == null ) {
- this.statusBar = createStatusBar();
- }
-
- return this.statusBar;
+ private TextField findTextField;
+
+ private DefinitionSource definitionSource;
+ private DefinitionPane definitionPane;
+ private FileEditorTabPane fileEditorPane;
+ private HTMLPreviewPane previewPane;
+
+ /**
+ * Prevent re-instantiation processing classes.
+ */
+ private Map<FileEditorTab, Processor<String>> processors;
+
+ public MainWindow() {
+ initLayout();
+ initFindInput();
+ initSnitch();
+ initDefinitionListener();
+ initTabAddedListener();
+ initTabChangedListener();
+ initPreferences();
+ }
+
+ /**
+ * Watch for changes to external files. In particular, this awaits
+ * modifications to any XSL files associated with XML files being edited. When
+ * an XSL file is modified (external to the application), the snitch's ears
+ * perk up and the file is reloaded. This keeps the XSL transformation up to
+ * date with what's on the file system.
+ */
+ private void initSnitch() {
+ getSnitch().addObserver( this );
+ }
+
+ /**
+ * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
+ * presses.
+ */
+ private void initFindInput() {
+ final TextField input = getFindTextField();
+
+ input.setOnKeyPressed( (KeyEvent event) -> {
+ switch( event.getCode() ) {
+ case F3:
+ case ENTER:
+ findNext();
+ break;
+ case F:
+ if( !event.isControlDown() ) {
+ break;
+ }
+ case ESCAPE:
+ getStatusBar().setGraphic( null );
+ getActiveFileEditor().getEditorPane().requestFocus();
+ break;
+ }
+ } );
+
+ // Remove when the input field loses focus.
+ input.focusedProperty().addListener(
+ (
+ final ObservableValue<? extends Boolean> focused,
+ final Boolean oFocus,
+ final Boolean nFocus) -> {
+ if( !nFocus ) {
+ getStatusBar().setGraphic( null );
+ }
+ }
+ );
+ }
+
+ /**
+ * Listen for file editor tab pane to receive an open definition source event.
+ */
+ private void initDefinitionListener() {
+ getFileEditorPane().onOpenDefinitionFileProperty().addListener(
+ (ObservableValue<? extends Path> definitionFile,
+ final Path oldPath, final Path newPath) -> {
+ openDefinition( newPath );
+
+ // Indirectly refresh the resolved map.
+ setProcessors( null );
+
+ updateDefinitionPane();
+
+ try {
+ getSnitch().ignore( oldPath );
+ getSnitch().listen( newPath );
+ } catch( final IOException ex ) {
+ error( ex );
+ }
+
+ // Will create new processors and therefore a new resolved map.
+ refreshSelectedTab( getActiveFileEditor() );
+ }
+ );
+ }
+
+ /**
+ * When tabs are added, hook the various change listeners onto the new tab so
+ * that the preview pane refreshes as necessary.
+ */
+ private void initTabAddedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Make sure the text processor kicks off when new files are opened.
+ final ObservableList<Tab> tabs = editorPane.getTabs();
+
+ // Update the preview pane on tab changes.
+ tabs.addListener(
+ (final Change<? extends Tab> change) -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ // Multiple tabs can be added simultaneously.
+ for( final Tab newTab : change.getAddedSubList() ) {
+ final FileEditorTab tab = (FileEditorTab)newTab;
+
+ initTextChangeListener( tab );
+ initCaretParagraphListener( tab );
+ initVariableNameInjector( tab );
+// initSyntaxListener( tab );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Reloads the preferences from the previous load.
+ */
+ private void initPreferences() {
+ restoreDefinitionSource();
+ getFileEditorPane().restorePreferences();
+ updateDefinitionPane();
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabSelectionListener(
+ (ObservableValue<? extends Tab> tabPane,
+ final Tab oldTab, final Tab newTab) -> {
+
+ // If there was no old tab, then this is a first time load, which
+ // can be ignored.
+ if( oldTab != null ) {
+ if( newTab == null ) {
+ closeRemainingTab();
+ }
+ else {
+ // Update the preview with the edited text.
+ refreshSelectedTab( (FileEditorTab)newTab );
+ }
+ }
+ }
+ );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener(
+ (ObservableValue<? extends String> editor,
+ final String oldValue, final String newValue) -> {
+ refreshSelectedTab( tab );
+ }
+ );
+ }
+
+ private void initCaretParagraphListener( final FileEditorTab tab ) {
+ tab.addCaretParagraphListener(
+ (ObservableValue<? extends Integer> editor,
+ final Integer oldValue, final Integer newValue) -> {
+ refreshSelectedTab( tab );
+ }
+ );
+ }
+
+ private void initVariableNameInjector( final FileEditorTab tab ) {
+ VariableNameInjector.listen( tab, getDefinitionPane() );
+ }
+
+ /**
+ * Called whenever the preview pane becomes out of sync with the file editor
+ * tab. This can be called when the text changes, the caret paragraph changes,
+ * or the file tab changes.
+ *
+ * @param tab The file editor tab that has been changed in some fashion.
+ */
+ private void refreshSelectedTab( final FileEditorTab tab ) {
+ if( tab.isFileOpen() ) {
+ getPreviewPane().setPath( tab.getPath() );
+
+ // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
+ final Position p = tab.getCaretOffset();
+ getLineNumberText().setText(
+ get( STATUS_BAR_LINE,
+ p.getMajor() + 1,
+ p.getMinor() + 1,
+ tab.getCaretPosition() + 1
+ )
+ );
+
+ Processor<String> processor = getProcessors().get( tab );
+
+ if( processor == null ) {
+ processor = createProcessor( tab );
+ getProcessors().put( tab, processor );
+ }
+
+ try {
+ getNotifier().clear();
+ processor.processChain( tab.getEditorText() );
+ } catch( final Exception ex ) {
+ error( ex );
+ }
+ }
+ }
+
+ /**
+ * Used to find text in the active file editor window.
+ */
+ private void find() {
+ final TextField input = getFindTextField();
+ getStatusBar().setGraphic( input );
+ input.requestFocus();
+ }
+
+ public void findNext() {
+ getActiveFileEditor().searchNext( getFindTextField().getText() );
+ }
+
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
+ private Map<String, String> getResolvedMap() {
+ return getDefinitionSource().getResolvedMap();
+ }
+
+ /**
+ * Returns the root node for the hierarchical definition source.
+ *
+ * @return Data to display in the definition pane.
+ */
+ private TreeView<String> getTreeView() {
+ try {
+ return getDefinitionSource().asTreeView();
+ } catch( Exception e ) {
+ error( e );
+ }
+
+ // Slightly redundant as getDefinitionSource() might have returned an
+ // empty definition source.
+ return (new EmptyDefinitionSource()).asTreeView();
+ }
+
+ /**
+ * Called when a definition source is opened.
+ *
+ * @param path Path to the definition source that was opened.
+ */
+ private void openDefinition( final Path path ) {
+ try {
+ final DefinitionSource ds = createDefinitionSource( path.toString() );
+ setDefinitionSource( ds );
+ storeDefinitionSource();
+ updateDefinitionPane();
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private void updateDefinitionPane() {
+ getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
+ }
+
+ private void restoreDefinitionSource() {
+ final Preferences preferences = getPreferences();
+ final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
+
+ // If there's no definition source set, don't try to load it.
+ if( source != null ) {
+ setDefinitionSource( createDefinitionSource( source ) );
+ }
+ }
+
+ private void storeDefinitionSource() {
+ final Preferences preferences = getPreferences();
+ final DefinitionSource ds = getDefinitionSource();
+
+ preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
+ }
+
+ /**
+ * Called when the last open tab is closed to clear the preview pane.
+ */
+ private void closeRemainingTab() {
+ getPreviewPane().clear();
+ }
+
+ /**
+ * Called when an exception occurs that warrants the user's attention.
+ *
+ * @param e The exception with a message that the user should know about.
+ */
+ private void error( final Exception e ) {
+ getNotifier().notify( e );
+ }
+
+ //---- File actions -------------------------------------------------------
+ /**
+ * Called when an observable instance has changed. This is called by both the
+ * snitch service and the notify service. The snitch service can be called for
+ * different file types, including definition sources.
+ *
+ * @param observable The observed instance.
+ * @param value The noteworthy item.
+ */
+ @Override
+ public void update( final Observable observable, final Object value ) {
+ if( value != null ) {
+ if( observable instanceof Snitch && value instanceof Path ) {
+ final Path path = (Path)value;
+ final FileTypePredicate predicate
+ = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
+
+ // Reload definitions.
+ if( predicate.test( path.toFile() ) ) {
+ updateDefinitionSource( path );
+ }
+
+ updateSelectedTab();
+ }
+ else if( observable instanceof Notifier && value instanceof String ) {
+ updateStatusBar( (String)value );
+ }
+ }
+ }
+
+ /**
+ * Updates the status bar to show the given message.
+ *
+ * @param s The message to show in the status bar.
+ */
+ private void updateStatusBar( final String s ) {
+ Platform.runLater(
+ () -> {
+ final int index = s.indexOf( '\n' );
+ final String message = s.substring( 0, index > 0 ? index : s.length() );
+
+ getStatusBar().setText( message );
+ }
+ );
+ }
+
+ /**
+ * Called when a file has been modified.
+ *
+ * @param file Path to the modified file.
+ */
+ private void updateSelectedTab() {
+ Platform.runLater(
+ () -> {
+ // Brute-force XSLT file reload by re-instantiating all processors.
+ resetProcessors();
+ refreshSelectedTab( getActiveFileEditor() );
+ }
+ );
+ }
+
+ /**
+ * Reloads the definition source from the given path.
+ *
+ * @param path The path containing new definition information.
+ */
+ private void updateDefinitionSource( final Path path ) {
+ Platform.runLater(
+ () -> {
+ openDefinition( path );
+ }
+ );
+ }
+
+ /**
+ * After resetting the processors, they will refresh anew to be up-to-date
+ * with the files (text and definition) currently loaded into the editor.
+ */
+ private void resetProcessors() {
+ getProcessors().clear();
+ }
+
+ //---- File actions -------------------------------------------------------
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditor(), true );
+ }
+
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditor() );
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Help actions -------------------------------------------------------
+ private void helpAbout() {
+ Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( get( "Dialog.about.title" ) );
+ alert.setHeaderText( get( "Dialog.about.header" ) );
+ alert.setContentText( get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ //---- Convenience accessors ----------------------------------------------
+ private float getFloat( final String key, final float defaultValue ) {
+ return getPreferences().getFloat( key, defaultValue );
+ }
+
+ private Preferences getPreferences() {
+ return getOptions().getState();
+ }
+
+ private TextField createFindTextField() {
+ return new TextField();
+ }
+
+ protected Scene getScene() {
+ if( this.scene == null ) {
+ this.scene = createScene();
+ }
+
+ return this.scene;
+ }
+
+ public Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ private MarkdownEditorPane getActiveEditor() {
+ final EditorPane pane = getActiveFileEditor().getEditorPane();
+
+ return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
+ }
+
+ private FileEditorTab getActiveFileEditor() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ //---- Member accessors ---------------------------------------------------
+ private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
+ this.processors = map;
+ }
+
+ private Map<FileEditorTab, Processor<String>> getProcessors() {
+ if( this.processors == null ) {
+ setProcessors( new HashMap<>() );
+ }
+
+ return this.processors;
+ }
+
+ private FileEditorTabPane getFileEditorPane() {
+ if( this.fileEditorPane == null ) {
+ this.fileEditorPane = createFileEditorPane();
+ }
+
+ return this.fileEditorPane;
+ }
+
+ private HTMLPreviewPane getPreviewPane() {
+ if( this.previewPane == null ) {
+ this.previewPane = createPreviewPane();
+ }
+
+ return this.previewPane;
+ }
+
+ private void setDefinitionSource( final DefinitionSource definitionSource ) {
+ this.definitionSource = definitionSource;
+ }
+
+ private DefinitionSource getDefinitionSource() {
+ if( this.definitionSource == null ) {
+ this.definitionSource = new EmptyDefinitionSource();
+ }
+
+ return this.definitionSource;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ if( this.definitionPane == null ) {
+ this.definitionPane = createDefinitionPane();
+ }
+
+ return this.definitionPane;
+ }
+
+ private Options getOptions() {
+ return this.options;
+ }
+
+ private Snitch getSnitch() {
+ return this.snitch;
+ }
+
+ private Notifier getNotifier() {
+ return this.notifier;
+ }
+
+ public void setMenuBar( final MenuBar menuBar ) {
+ this.menuBar = menuBar;
+ }
+
+ public MenuBar getMenuBar() {
+ return this.menuBar;
+ }
+
+ private Text getLineNumberText() {
+ if( this.lineNumberText == null ) {
+ this.lineNumberText = createLineNumberText();
+ }
+
+ return this.lineNumberText;
+ }
+
+ private synchronized StatusBar getStatusBar() {
+ if( this.statusBar == null ) {
+ this.statusBar = createStatusBar();
+ }
+
+ return this.statusBar;
+ }
+
+ private TextField getFindTextField() {
+ if( this.findTextField == null ) {
+ this.findTextField = createFindTextField();
+ }
+
+ return this.findTextField;
}
src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
import java.io.InputStream;
import java.io.Writer;
-import java.security.InvalidParameterException;
-import java.text.MessageFormat;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.yaml.snakeyaml.DumperOptions;
-
-/**
- * <p>
- * This program loads a YAML document into memory, scans for variable
- * declarations, then substitutes any self-referential values back into the
- * document. Its output is the given YAML document without any variables.
- * Variables in the YAML document are denoted using a bracketed dollar symbol
- * syntax. For example: $field.name$. Some nomenclature to keep from going
- * squirrely, consider:
- * </p>
- *
- * <pre>
- * root:
- * node:
- * name: $field.name$
- * field:
- * name: Alan Turing
- * </pre>
- *
- * The various components of the given YAML are called:
- *
- * <ul>
- * <li><code>$field.name$</code> - delimited reference</li>
- * <li><code>field.name</code> - reference</li>
- * <li><code>name</code> - YAML field</li>
- * <li><code>Alan Turing</code> - (dereferenced) field value</li>
- * </ul>
- *
- * @author White Magic Software, Ltd.
- */
-public class YamlParser {
-
- /**
- * Separates YAML variable nodes (e.g., the dots in
- * <code>$root.node.var$</code>).
- */
- public static final String SEPARATOR = ".";
- public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 );
-
- private final static int GROUP_DELIMITED = 1;
- private final static int GROUP_REFERENCE = 2;
-
- private final static VariableDecorator VARIABLE_DECORATOR
- = new YamlVariableDecorator();
-
- /**
- * Compiled version of DEFAULT_REGEX.
- */
- private final static Pattern REGEX_PATTERN
- = Pattern.compile( YamlVariableDecorator.REGEX );
-
- /**
- * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
- */
- private final static char SEPARATOR_YAML = '/';
-
- /**
- * Start of the Universe (the YAML document node that contains all others).
- */
- private JsonNode documentRoot;
-
- /**
- * Map of references to dereferenced field values.
- */
- private Map<String, String> references;
-
- public YamlParser( final InputStream in ) throws IOException {
- process( in );
- }
-
- /**
- * Returns the given string with all the delimited references swapped with
- * their recursively resolved values.
- *
- * @param text The text to parse with zero or more delimited references to
- * replace.
- *
- * @return The substituted value.
- */
- public String substitute( String text ) {
- final Matcher matcher = patternMatch( text );
- final Map<String, String> map = getReferences();
-
- while( matcher.find() ) {
- final String key = matcher.group( GROUP_DELIMITED );
- final String value = map.get( key );
-
- if( value == null ) {
- missing( text );
- } else {
- text = text.replace( key, value );
- }
- }
-
- return text;
- }
-
- /**
- * Returns all the strings with their values resolved in a flat hierarchy.
- * This copies all the keys and resolved values into a new map.
- *
- * @return The new map created with all values having been resolved,
- * recursively.
- */
- public Map<String, String> createResolvedMap() {
- final Map<String, String> map = new HashMap<>( 1024 );
-
- resolve( getDocumentRoot(), "", map );
-
- return map;
- }
-
- /**
- * 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 map Container that associates definitions with values.
- */
- private void resolve(
- final JsonNode rootNode, final String path, final Map<String, String> map ) {
-
- if( rootNode != null ) {
- rootNode.fields().forEachRemaining(
- (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map )
- );
- }
- }
-
- /**
- * Recursively adapt each rootNode to a corresponding rootItem.
- *
- * @param rootNode The node to adapt.
- */
- private void resolve(
- final Entry<String, JsonNode> rootNode,
- final String path,
- final Map<String, String> map ) {
-
- final JsonNode leafNode = rootNode.getValue();
- final String key = rootNode.getKey();
-
- if( leafNode.isValueNode() ) {
- final String value = rootNode.getValue().asText();
-
- map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) );
- }
-
- if( leafNode.isObject() ) {
- resolve( leafNode, path + key + SEPARATOR, map );
- }
- }
-
- /**
- * Reads the first document from the given stream of YAML data and returns a
- * corresponding object that represents the YAML hierarchy. The calling class
- * is responsible for closing the stream. Calling classes should use
- * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
- *
- * @param in The input stream containing YAML content.
- *
- * @return An object hierarchy to represent the content.
- *
- * @throws IOException Could not read the stream.
- */
- private JsonNode process( final InputStream in ) throws IOException {
-
- final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in );
- setDocumentRoot( root );
- process( root );
- return getDocumentRoot();
- }
-
- /**
- * Iterate over a given root node (at any level of the tree) and process each
- * leaf node.
- *
- * @param root A node to process.
- */
- private void process( final JsonNode root ) {
- root.fields().forEachRemaining( this::process );
- }
-
- /**
- * Process the given field, which is a named node. This is where the
- * application does the up-front work of mapping references to their fully
- * recursively dereferenced values.
- *
- * @param field The named node.
- */
- private void process( final Entry<String, JsonNode> field ) {
- final JsonNode node = field.getValue();
-
- if( node.isObject() ) {
- process( node );
- } else {
- final JsonNode fieldValue = field.getValue();
-
- // Only basic data types can be parsed into variable values. For
- // node structures, YAML has a built-in mechanism.
- if( fieldValue.isValueNode() ) {
- try {
- resolve( fieldValue.asText() );
- } catch( StackOverflowError e ) {
- throw new IllegalArgumentException(
- "Unresolvable: " + node.textValue() + " = " + fieldValue );
- }
- }
- }
- }
-
- /**
- * Inserts the delimited references and field values into the cache. This will
- * overwrite existing references.
- *
- * @param fieldValue YAML field containing zero or more delimited references.
- * If it contains a delimited reference, the parameter is modified with the
- * dereferenced value before it is returned.
- *
- * @return fieldValue without delimited references.
- */
- private String resolve( String fieldValue ) {
- final Matcher matcher = patternMatch( fieldValue );
-
- while( matcher.find() ) {
- final String delimited = matcher.group( GROUP_DELIMITED );
- final String reference = matcher.group( GROUP_REFERENCE );
- final String dereference = resolve( lookup( reference ) );
-
- fieldValue = fieldValue.replace( delimited, dereference );
-
- // This will perform some superfluous calls by overwriting existing
- // items in the delimited reference map.
- put( delimited, dereference );
- }
-
- return fieldValue;
- }
-
- /**
- * Inserts a key/value pair into the references map. The map retains
- * references and dereferenced values found in the YAML. If the reference
- * already exists, this will overwrite with a new value.
- *
- * @param delimited The variable name.
- * @param dereferenced The resolved value.
- */
- private void put( String delimited, String dereferenced ) {
- if( dereferenced.isEmpty() ) {
- missing( delimited );
- } else {
- getReferences().put( delimited, dereferenced );
- }
- }
-
- /**
- * Writes the modified YAML document to standard output.
- */
- private void writeDocument() throws IOException {
- getObjectMapper().writeValue( System.out, getDocumentRoot() );
- }
-
- /**
- * Called when a delimited reference is dereferenced to an empty string. This
- * should produce a warning for the user.
- *
- * @param delimited Delimited reference with no derived value.
- */
- private void missing( final String delimited ) {
- throw new InvalidParameterException(
- MessageFormat.format( "Missing value for '{0}'.", delimited ) );
- }
-
- /**
- * Returns a REGEX_PATTERN matcher for the given text.
- *
- * @param text The text that contains zero or more instances of a
- * REGEX_PATTERN that can be found using the regular expression.
- */
- private Matcher patternMatch( String text ) {
- return getPattern().matcher( text );
- }
-
- /**
- * Finds the YAML value for a reference.
- *
- * @param reference References a value in the YAML document.
- *
- * @return The dereferenced value.
- */
- private String lookup( final String reference ) {
- return getDocumentRoot().at( asPath( reference ) ).asText();
- }
-
- /**
- * Converts a reference (not delimited) to a path that can be used to find a
- * value that should exist inside the YAML document.
- *
- * @param reference The reference to convert to a YAML document path.
- *
- * @return The reference with a leading slash and its separator characters
- * converted to slashes.
- */
- private String asPath( final String reference ) {
- return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML );
- }
-
- /**
- * Sets the parent node for the entire YAML document tree.
- *
- * @param documentRoot The parent node.
- */
- private void setDocumentRoot( ObjectNode documentRoot ) {
- this.documentRoot = documentRoot;
- }
-
- /**
- * Returns the parent node for the entire YAML document tree.
- *
- * @return The parent node.
- */
- protected JsonNode getDocumentRoot() {
- return this.documentRoot;
- }
-
- /**
- * Returns the compiled regular expression REGEX_PATTERN used to match
- * delimited references.
- *
- * @return A compiled regex for use with the Matcher.
- */
- private Pattern getPattern() {
- return REGEX_PATTERN;
- }
-
- /**
- * Returns the list of references mapped to dereferenced values.
- *
- * @return
- */
- private Map<String, String> getReferences() {
- if( this.references == null ) {
- this.references = createReferences();
- }
-
- return this.references;
- }
-
- /**
- * Subclasses can override this method to insert their own map.
- *
- * @return An empty HashMap, never null.
- */
- protected Map<String, String> createReferences() {
- return new HashMap<>();
- }
-
- private final class ResolverYAMLFactory extends YAMLFactory {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- protected YAMLGenerator _createGenerator(
- final Writer out, final IOContext ctxt ) throws IOException {
-
- return new ResolverYAMLGenerator(
- ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
- out, _version );
- }
- }
-
- private class ResolverYAMLGenerator extends YAMLGenerator {
-
- public ResolverYAMLGenerator(
- final IOContext ctxt,
- final int jsonFeatures,
- final int yamlFeatures,
- final ObjectCodec codec,
- final Writer out,
- final DumperOptions.Version version ) throws IOException {
-
- super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
- }
-
- @Override
- public void writeString( final String text )
- throws IOException, JsonGenerationException {
- super.writeString( substitute( text ) );
- }
- }
-
- private YAMLFactory getYAMLFactory() {
- return new ResolverYAMLFactory();
- }
-
- private ObjectMapper getObjectMapper() {
- return new ObjectMapper( getYAMLFactory() );
- }
-
- /**
- * Returns the character used to separate YAML paths within delimited
- * references. This will return only the first character of the command line
- * parameter, if the default is overridden.
- *
- * @return A period by default.
- */
- private char getDelimitedSeparator() {
- return SEPARATOR.charAt( 0 );
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.yaml.snakeyaml.DumperOptions;
+
+/**
+ * <p>
+ * This program loads a YAML document into memory, scans for variable
+ * declarations, then substitutes any self-referential values back into the
+ * document. Its output is the given YAML document without any variables.
+ * Variables in the YAML document are denoted using a bracketed dollar symbol
+ * syntax. For example: $field.name$. Some nomenclature to keep from going
+ * squirrely, consider:
+ * </p>
+ *
+ * <pre>
+ * root:
+ * node:
+ * name: $field.name$
+ * field:
+ * name: Alan Turing
+ * </pre>
+ *
+ * The various components of the given YAML are called:
+ *
+ * <ul>
+ * <li><code>$field.name$</code> - delimited reference</li>
+ * <li><code>field.name</code> - reference</li>
+ * <li><code>name</code> - YAML field</li>
+ * <li><code>Alan Turing</code> - (dereferenced) field value</li>
+ * </ul>
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class YamlParser {
+
+ /**
+ * Separates YAML variable nodes (e.g., the dots in
+ * <code>$root.node.var$</code>).
+ */
+ public static final String SEPARATOR = ".";
+ public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 );
+
+ private final static int GROUP_DELIMITED = 1;
+ private final static int GROUP_REFERENCE = 2;
+
+ private final static VariableDecorator VARIABLE_DECORATOR
+ = new YamlVariableDecorator();
+
+ private String error;
+
+ /**
+ * Compiled version of DEFAULT_REGEX.
+ */
+ private final static Pattern REGEX_PATTERN
+ = Pattern.compile( YamlVariableDecorator.REGEX );
+
+ /**
+ * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
+ */
+ private final static char SEPARATOR_YAML = '/';
+
+ /**
+ * Start of the Universe (the YAML document node that contains all others).
+ */
+ private JsonNode documentRoot;
+
+ /**
+ * Map of references to dereferenced field values.
+ */
+ private Map<String, String> references;
+
+ public YamlParser( final InputStream in ) throws IOException {
+ process( in );
+ }
+
+ /**
+ * Returns the given string with all the delimited references swapped with
+ * their recursively resolved values.
+ *
+ * @param text The text to parse with zero or more delimited references to
+ * replace.
+ *
+ * @return The substituted value.
+ */
+ public String substitute( String text ) {
+ final Matcher matcher = patternMatch( text );
+ final Map<String, String> map = getReferences();
+
+ while( matcher.find() ) {
+ final String key = matcher.group( GROUP_DELIMITED );
+ final String value = map.get( key );
+
+ if( value == null ) {
+ missing( text );
+ }
+ else {
+ text = text.replace( key, value );
+ }
+ }
+
+ return text;
+ }
+
+ /**
+ * Returns all the strings with their values resolved in a flat hierarchy.
+ * This copies all the keys and resolved values into a new map.
+ *
+ * @return The new map created with all values having been resolved,
+ * recursively.
+ */
+ public Map<String, String> createResolvedMap() {
+ final Map<String, String> map = new HashMap<>( 1024 );
+
+ resolve( getDocumentRoot(), "", map );
+
+ return map;
+ }
+
+ /**
+ * 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 map Container that associates definitions with values.
+ */
+ private void resolve(
+ final JsonNode rootNode, final String path, final Map<String, String> map ) {
+
+ if( rootNode != null ) {
+ rootNode.fields().forEachRemaining(
+ (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map )
+ );
+ }
+ }
+
+ /**
+ * Recursively adapt each rootNode to a corresponding rootItem.
+ *
+ * @param rootNode The node to adapt.
+ */
+ private void resolve(
+ final Entry<String, JsonNode> rootNode,
+ final String path,
+ final Map<String, String> map ) {
+
+ final JsonNode leafNode = rootNode.getValue();
+ final String key = rootNode.getKey();
+
+ if( leafNode.isValueNode() ) {
+ final String value = rootNode.getValue().asText();
+
+ map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) );
+ }
+
+ if( leafNode.isObject() ) {
+ resolve( leafNode, path + key + SEPARATOR, map );
+ }
+ }
+
+ /**
+ * Reads the first document from the given stream of YAML data and returns a
+ * corresponding object that represents the YAML hierarchy. The calling class
+ * is responsible for closing the stream. Calling classes should use
+ * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
+ *
+ * @param in The input stream containing YAML content.
+ *
+ * @return An object hierarchy to represent the content.
+ *
+ * @throws IOException Could not read the stream.
+ */
+ private JsonNode process( final InputStream in ) throws IOException {
+ final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in );
+ setDocumentRoot( root );
+ process( root );
+ return getDocumentRoot();
+ }
+
+ /**
+ * Iterate over a given root node (at any level of the tree) and process each
+ * leaf node.
+ *
+ * @param root A node to process.
+ */
+ private void process( final JsonNode root ) {
+ root.fields().forEachRemaining( this::process );
+ }
+
+ /**
+ * Process the given field, which is a named node. This is where the
+ * application does the up-front work of mapping references to their fully
+ * recursively dereferenced values.
+ *
+ * @param field The named node.
+ */
+ private void process( final Entry<String, JsonNode> field ) {
+ final JsonNode node = field.getValue();
+
+ if( node.isObject() ) {
+ process( node );
+ }
+ else {
+ final JsonNode fieldValue = field.getValue();
+
+ // Only basic data types can be parsed into variable values. For
+ // node structures, YAML has a built-in mechanism.
+ if( fieldValue.isValueNode() ) {
+ try {
+ resolve( fieldValue.asText() );
+ } catch( StackOverflowError e ) {
+ setError( "Unresolvable: " + node.textValue() + " = " + fieldValue );
+ }
+ }
+ }
+ }
+
+ /**
+ * Inserts the delimited references and field values into the cache. This will
+ * overwrite existing references.
+ *
+ * @param fieldValue YAML field containing zero or more delimited references.
+ * If it contains a delimited reference, the parameter is modified with the
+ * dereferenced value before it is returned.
+ *
+ * @return fieldValue without delimited references.
+ */
+ private String resolve( String fieldValue ) {
+ final Matcher matcher = patternMatch( fieldValue );
+
+ while( matcher.find() ) {
+ final String delimited = matcher.group( GROUP_DELIMITED );
+ final String reference = matcher.group( GROUP_REFERENCE );
+ final String dereference = resolve( lookup( reference ) );
+
+ fieldValue = fieldValue.replace( delimited, dereference );
+
+ // This will perform some superfluous calls by overwriting existing
+ // items in the delimited reference map.
+ put( delimited, dereference );
+ }
+
+ return fieldValue;
+ }
+
+ /**
+ * Inserts a key/value pair into the references map. The map retains
+ * references and dereferenced values found in the YAML. If the reference
+ * already exists, this will overwrite with a new value.
+ *
+ * @param delimited The variable name.
+ * @param dereferenced The resolved value.
+ */
+ private void put( String delimited, String dereferenced ) {
+ if( dereferenced.isEmpty() ) {
+ missing( delimited );
+ }
+ else {
+ getReferences().put( delimited, dereferenced );
+ }
+ }
+
+ /**
+ * Writes the modified YAML document to standard output.
+ */
+ private void writeDocument() throws IOException {
+ getObjectMapper().writeValue( System.out, getDocumentRoot() );
+ }
+
+ /**
+ * Called when a delimited reference is dereferenced to an empty string. This
+ * should produce a warning for the user.
+ *
+ * @param delimited Delimited reference with no derived value.
+ */
+ private void missing( final String delimited ) {
+ setError( MessageFormat.format( "Missing value for '{0}'.", delimited ) );
+ }
+
+ /**
+ * Returns a REGEX_PATTERN matcher for the given text.
+ *
+ * @param text The text that contains zero or more instances of a
+ * REGEX_PATTERN that can be found using the regular expression.
+ */
+ private Matcher patternMatch( String text ) {
+ return getPattern().matcher( text );
+ }
+
+ /**
+ * Finds the YAML value for a reference.
+ *
+ * @param reference References a value in the YAML document.
+ *
+ * @return The dereferenced value.
+ */
+ private String lookup( final String reference ) {
+ return getDocumentRoot().at( asPath( reference ) ).asText();
+ }
+
+ /**
+ * Converts a reference (not delimited) to a path that can be used to find a
+ * value that should exist inside the YAML document.
+ *
+ * @param reference The reference to convert to a YAML document path.
+ *
+ * @return The reference with a leading slash and its separator characters
+ * converted to slashes.
+ */
+ private String asPath( final String reference ) {
+ return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML );
+ }
+
+ /**
+ * Sets the parent node for the entire YAML document tree.
+ *
+ * @param documentRoot The parent node.
+ */
+ private void setDocumentRoot( ObjectNode documentRoot ) {
+ this.documentRoot = documentRoot;
+ }
+
+ /**
+ * Returns the parent node for the entire YAML document tree.
+ *
+ * @return The parent node.
+ */
+ protected JsonNode getDocumentRoot() {
+ return this.documentRoot;
+ }
+
+ /**
+ * Returns the compiled regular expression REGEX_PATTERN used to match
+ * delimited references.
+ *
+ * @return A compiled regex for use with the Matcher.
+ */
+ private Pattern getPattern() {
+ return REGEX_PATTERN;
+ }
+
+ /**
+ * Returns the list of references mapped to dereferenced values.
+ *
+ * @return
+ */
+ private Map<String, String> getReferences() {
+ if( this.references == null ) {
+ this.references = createReferences();
+ }
+
+ return this.references;
+ }
+
+ /**
+ * Subclasses can override this method to insert their own map.
+ *
+ * @return An empty HashMap, never null.
+ */
+ protected Map<String, String> createReferences() {
+ return new HashMap<>();
+ }
+
+ private final class ResolverYAMLFactory extends YAMLFactory {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected YAMLGenerator _createGenerator(
+ final Writer out, final IOContext ctxt ) throws IOException {
+
+ return new ResolverYAMLGenerator(
+ ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
+ out, _version );
+ }
+ }
+
+ private class ResolverYAMLGenerator extends YAMLGenerator {
+
+ public ResolverYAMLGenerator(
+ final IOContext ctxt,
+ final int jsonFeatures,
+ final int yamlFeatures,
+ final ObjectCodec codec,
+ final Writer out,
+ final DumperOptions.Version version ) throws IOException {
+
+ super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
+ }
+
+ @Override
+ public void writeString( final String text )
+ throws IOException, JsonGenerationException {
+ super.writeString( substitute( text ) );
+ }
+ }
+
+ private YAMLFactory getYAMLFactory() {
+ return new ResolverYAMLFactory();
+ }
+
+ private ObjectMapper getObjectMapper() {
+ return new ObjectMapper( getYAMLFactory() );
+ }
+
+ /**
+ * Returns the character used to separate YAML paths within delimited
+ * references. This will return only the first character of the command line
+ * parameter, if the default is overridden.
+ *
+ * @return A period by default.
+ */
+ private char getDelimitedSeparator() {
+ return SEPARATOR.charAt( 0 );
+ }
+
+ private void setError( final String error ) {
+ this.error = error;
+ }
+
+ /**
+ * Returns the last error message, if any, that occurred during parsing.
+ *
+ * @return The error message or the empty string if no error occurred.
+ */
+ public String getError() {
+ return this.error == null ? "" : this.error;
}
}
src/main/resources/com/scrivenvar/build.sh
#!/bin/bash
-INKSCAPE=/usr/bin/inkscape
+INKSCAPE="/usr/bin/inkscape"
+PNG_COMPRESS="optipng"
+PNG_COMPRESS_OPTS="-o9 *png"
+ICO_TOOL="icotool"
+ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
declare -a SIZES=("16" "32" "64" "128" "256" "512")
for i in "${SIZES[@]}"; do
# -y: export background opacity 0
$INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png"
done
+
+# Compess the PNG images.
+which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
+
+# Generate an ICO file.
+which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
src/main/resources/com/scrivenvar/logo128.png
Binary files differ
src/main/resources/com/scrivenvar/logo256.png
Binary files differ
src/main/resources/com/scrivenvar/logo32.png
Binary files differ
src/main/resources/com/scrivenvar/logo512.png
Binary files differ
src/main/resources/com/scrivenvar/logo64.png
Binary files differ
Delta1015 lines added, 969 lines removed, 46-line increase