Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M README.md
1616
* Platform independent (Windows, Linux, MacOS)
1717
* Auto-insert variable names pressing `Control+Space`
18
* Real-time XSLT2 transformation of XML documents
1819
1920
Future Features
2021
---
2122
* Spell check
2223
* Search and replace, with or without variables
23
* XML and XSL processing
2424
* R integration using [Rserve](https://rforge.net/Rserve/)
2525
* Re-organize variable names
M build.gradle
1
version = '1.0.0'
1
version = '1.0.2'
22
33
apply plugin: 'java'
44
apply plugin: 'java-library-distribution'
55
apply plugin: 'application'
66
7
sourceCompatibility = 1.8
7
sourceCompatibility = JavaVersion.VERSION_1_8
8
9
applicationName = 'scrivenvar'
810
911
mainClassName = 'com.scrivenvar.Main'
1012
1113
repositories {
12
	jcenter()
14
  jcenter()
1315
}
1416
1517
compileJava {
1618
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
1719
}
1820
1921
dependencies {
20
  compile group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.7-M2'
21
	compile group: 'com.miglayout', name: 'miglayout-javafx', version: '5.0'
22
	compile group: 'de.jensd', name: 'fontawesomefx-fontawesome', version: '4.5.0'
23
  compile group: 'org.ahocorasick', name: 'ahocorasick', version: '0.3.0'
24
  compile group: 'com.vladsch.flexmark', name: 'flexmark', version: '0.6.1'
25
  compile group: 'com.vladsch.flexmark', name: 'flexmark-ext-gfm-tables', version: '0.6.1'
26
  compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.8.4'
27
  compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.8.4'
28
  compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.8.4'
29
  compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: '2.8.4'
30
  compile group: 'org.yaml', name: 'snakeyaml', version: '1.17'
31
  compile group: 'com.ximpleware', name: 'vtd-xml', version: '2.13'
32
  compile group: 'net.sf.saxon', name: 'Saxon-HE', version: '9.7.0-14'
33
  compile group: 'com.googlecode.juniversalchardet', name: 'juniversalchardet', version: '1.0.3'
34
  compile group: 'org.apache.commons', name: 'commons-configuration2', version: '2.1'
22
  compile 'org.fxmisc.richtext:richtextfx:0.7-M2'
23
  compile 'com.miglayout:miglayout-javafx:5.0'
24
  compile 'de.jensd:fontawesomefx-fontawesome:4.5.0'
25
  compile 'org.ahocorasick:ahocorasick:0.3.0'
26
  compile 'com.vladsch.flexmark:flexmark:0.6.1'
27
  compile 'com.vladsch.flexmark:flexmark-ext-gfm-tables:0.6.1'
28
  compile 'com.fasterxml.jackson.core:jackson-core:2.8.4'
29
  compile 'com.fasterxml.jackson.core:jackson-databind:2.8.4'
30
  compile 'com.fasterxml.jackson.core:jackson-annotations:2.8.4'
31
  compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.4'
32
  compile 'org.yaml:snakeyaml:1.17'
33
  compile 'com.ximpleware:vtd-xml:2.13'
34
  compile 'net.sf.saxon:Saxon-HE:9.7.0-14'
35
  compile 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
36
  compile 'org.apache.commons:commons-configuration2:2.1'
37
  compile files('libs/renjin-script-engine-0.8.2300-jar-with-dependencies.jar')
3538
}
3639
3740
jar {
38
	baseName = 'scrivenvar'
41
  baseName = applicationName
3942
  
40
  from {
41
    (configurations.runtime).collect {
42
      it.isDirectory() ? it : zipTree(it)
43
  doFirst {
44
    from {
45
      configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
4346
    }
4447
  }
45
    
46
	manifest {
47
		attributes 'Main-Class': mainClassName,
48
					'Class-Path': configurations.compile.collect { 'lib/' + it.getName() }.join(' ')
49
	}
48
49
  // Remove digital signature files to ensure an executable JAR file.
50
  exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' 
51
52
  manifest {
53
    attributes 'Main-Class': mainClassName
54
    attributes 'Class-Path': configurations.compile.collect { 'libs/' + it.getName() }.join(' ')
55
  }
5056
}
5157
5258
distributions {
53
	main {
54
		baseName = 'scrivenvar'
55
		contents {
56
			from { ['LICENSE', 'README.md'] }
57
			into( 'images' ) {
58
				from { 'images' }
59
			}
60
		}
61
	}
59
  main {
60
    baseName = applicationName
61
    contents {
62
      from { ['LICENSE', 'README.md'] }
63
      into( 'images' ) {
64
        from { 'images' }
65
      }
66
    }
67
  }
6268
}
6369
A libs/renjin-script-engine-0.8.2300-jar-with-dependencies.jar
Binary file
A src/main/java/com/scrivenvar/AbstractFileFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.predicates.files.FileTypePredicate;
31
import com.scrivenvar.service.Settings;
32
import java.nio.file.Path;
33
import java.util.Iterator;
34
import java.util.List;
35
36
/**
37
 * Provides common behaviours for factories that instantiate classes based on
38
 * file type.
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public class AbstractFileFactory {
43
44
  private final Settings settings = Services.load( Settings.class );
45
46
  /**
47
   * Creates a definition source that can read and write files that match the
48
   * given file type (from the path).
49
   *
50
   * @param path Reference to a variable definition file.
51
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
52
   *
53
   * @return The file type that corresponds to the given path.
54
   */
55
  protected FileType lookup( final Path path, final String prefix ) {
56
    final Settings properties = getSettings();
57
    final Iterator<String> keys = properties.getKeys( prefix );
58
59
    boolean found = false;
60
    FileType fileType = null;
61
62
    while( keys.hasNext() && !found ) {
63
      final String key = keys.next();
64
      final List<String> patterns = properties.getStringSettingList( key );
65
      final FileTypePredicate predicate = new FileTypePredicate( patterns );
66
67
      if( found = predicate.test( path.toFile() ) ) {
68
        // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
69
        // to a standard name (as defined in the settings.properties file).
70
        final String suffix = key.replace( prefix + ".", "" );
71
        fileType = FileType.from( suffix );
72
      }
73
    }
74
75
    return fileType;
76
  }
77
78
  /**
79
   * Throws IllegalArgumentException because the given path could not be
80
   * recognized.
81
   *
82
   * @param type The detected path type (protocol, file extension, etc.).
83
   * @param path The path to a source of definitions.
84
   */
85
  protected void unknownFileType( final String type, final String path ) {
86
    throw new IllegalArgumentException(
87
      "Unknown type '" + type + "' for '" + path + "'."
88
    );
89
  }
90
91
  /**
92
   * Throws IllegalArgumentException because the extension for the given path
93
   * could not be recognized.
94
   *
95
   * @param path The path to a file that could not be loaded.
96
   */
97
  protected void unknownExtension( final Path path ) {
98
    throw new IllegalArgumentException(
99
      "Unknown extension for '" + path + "'."
100
    );
101
  }
102
103
  /**
104
   * Return the singleton Settings instance.
105
   *
106
   * @return The settings for
107
   */
108
  private Settings getSettings() {
109
    return this.settings;
110
  }
111
112
}
1113
M src/main/java/com/scrivenvar/Constants.java
3838
3939
  /**
40
   * Prevent instantiation.
40
   * Prevent instantiation, deliberately.
4141
   */
4242
  private Constants() {
4343
  }
4444
4545
  private static String get( final String key ) {
4646
    return SETTINGS.getSetting( key, "" );
47
  }
48
49
  private static int get( final String key, int defaultValue ) {
50
    return SETTINGS.getSetting( key, defaultValue );
4751
  }
4852
4953
  // Bootstrapping...
5054
  public static final String SETTINGS_NAME = "/com/scrivenvar/settings.properties";
5155
5256
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
57
  public static final int APP_WATCHDOG_TIMEOUT = get( "application.watchdog.timeout", 100 );
5358
5459
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
...
6469
  public static final String CARET_POSITION_BASE = get( "caret.token.base" );
6570
  public static final String CARET_POSITION_MD = get( "caret.token.markdown" );
66
  public static final String CARET_POSITION_XML = get( "caret.token.xml" );
6771
  public static final String CARET_POSITION_HTML = get( "caret.token.html" );
6872
6973
  public static final String PREFS_ROOT = get( "preferences.root" );
7074
  public static final String PREFS_STATE = get( "preferences.root.state" );
7175
  public static final String PREFS_OPTIONS = get( "preferences.root.options" );
7276
  public static final String PREFS_DEFINITION_SOURCE = get( "preferences.root.definition.source" );
77
78
  // Refer to filename extension settings in the configuration file. Do not
79
  // terminate these prefixes with a period.
80
  public static final String GLOB_PREFIX_FILE = "file.ext";
81
  public static final String GLOB_PREFIX_DEFINITION = "definition." + GLOB_PREFIX_FILE;
82
83
  // Different definition source protocols.
84
  public static final String DEFINITION_PROTOCOL_UNKNOWN = "unknown";
85
  public static final String DEFINITION_PROTOCOL_FILE = "file";
7386
}
7487
M src/main/java/com/scrivenvar/FileEditorTab.java
4242
import javafx.beans.property.SimpleBooleanProperty;
4343
import javafx.beans.value.ChangeListener;
44
import javafx.beans.value.ObservableValue;
4445
import javafx.event.Event;
4546
import javafx.scene.Node;
4647
import javafx.scene.control.Tab;
4748
import javafx.scene.control.Tooltip;
4849
import javafx.scene.input.InputEvent;
4950
import javafx.scene.text.Text;
51
import org.fxmisc.richtext.StyleClassedTextArea;
5052
import org.fxmisc.undo.UndoManager;
5153
import org.fxmisc.wellbehaved.event.EventPattern;
...
181183
   */
182184
  public int getCaretPosition() {
183
    return getEditorPane().getEditor().getCaretPosition();
185
    return getEditor().getCaretPosition();
184186
  }
185
  
187
188
  /**
189
   * Allows observers to synchronize caret position changes.
190
   *
191
   * @return An observable caret property value.
192
   */
193
  public final ObservableValue<Integer> caretPositionProperty() {
194
    return getEditor().caretPositionProperty();
195
  }
196
197
  /**
198
   * Returns the text area associated with this tab.
199
   *
200
   * @return A text editor.
201
   */
202
  private StyleClassedTextArea getEditor() {
203
    return getEditorPane().getEditor();
204
  }
205
186206
  /**
187207
   * Returns true if the given path exactly matches this tab's path.
...
302322
    return new String( text, getEncoding() );
303323
  }
304
  
324
305325
  public Path getPath() {
306326
    return this.path;
M src/main/java/com/scrivenvar/FileEditorTabPane.java
2828
package com.scrivenvar;
2929
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.predicates.files.FileTypePredicate;
32
import com.scrivenvar.service.Options;
33
import com.scrivenvar.service.Settings;
34
import com.scrivenvar.service.events.AlertMessage;
35
import com.scrivenvar.service.events.AlertService;
36
import static com.scrivenvar.service.events.AlertService.NO;
37
import static com.scrivenvar.service.events.AlertService.YES;
38
import com.scrivenvar.util.Utils;
39
import java.io.File;
40
import java.nio.file.Path;
41
import java.util.ArrayList;
42
import java.util.List;
43
import java.util.function.Consumer;
44
import java.util.prefs.Preferences;
45
import java.util.stream.Collectors;
46
import javafx.beans.property.ReadOnlyBooleanProperty;
47
import javafx.beans.property.ReadOnlyBooleanWrapper;
48
import javafx.beans.property.ReadOnlyObjectProperty;
49
import javafx.beans.property.ReadOnlyObjectWrapper;
50
import javafx.beans.value.ChangeListener;
51
import javafx.beans.value.ObservableValue;
52
import javafx.collections.ListChangeListener;
53
import javafx.collections.ObservableList;
54
import javafx.event.Event;
55
import javafx.scene.Node;
56
import javafx.scene.control.Alert;
57
import javafx.scene.control.ButtonType;
58
import javafx.scene.control.Tab;
59
import javafx.scene.control.TabPane;
60
import javafx.scene.control.TabPane.TabClosingPolicy;
61
import javafx.scene.input.InputEvent;
62
import javafx.stage.FileChooser;
63
import javafx.stage.FileChooser.ExtensionFilter;
64
import javafx.stage.Window;
65
import org.fxmisc.richtext.StyledTextArea;
66
import org.fxmisc.wellbehaved.event.EventPattern;
67
import org.fxmisc.wellbehaved.event.InputMap;
68
69
/**
70
 * Tab pane for file editors.
71
 *
72
 * @author Karl Tauber and White Magic Software, Ltd.
73
 */
74
public final class FileEditorTabPane extends TabPane {
75
76
  private final static String FILTER_EXTENSIONS = "filter.file";
77
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter";
78
79
  private final Options options = Services.load( Options.class );
80
  private final Settings settings = Services.load( Settings.class );
81
  private final AlertService alertService = Services.load( AlertService.class );
82
83
  private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>();
84
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
86
87
  /**
88
   * Constructs a new file editor tab pane.
89
   */
90
  public FileEditorTabPane() {
91
    final ObservableList<Tab> tabs = getTabs();
92
93
    setFocusTraversable( false );
94
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
95
96
    addTabSelectionListener(
97
      (ObservableValue<? extends Tab> tabPane,
98
        final Tab oldTab, final Tab newTab) -> {
99
100
        if( newTab != null ) {
101
          activeFileEditor.set( (FileEditorTab)newTab );
102
        }
103
      }
104
    );
105
106
    final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> {
107
      for( final Tab tab : tabs ) {
108
        if( ((FileEditorTab)tab).isModified() ) {
109
          this.anyFileEditorModified.set( true );
110
          break;
111
        }
112
      }
113
    };
114
115
    tabs.addListener(
116
      (ListChangeListener<Tab>)change -> {
117
        while( change.next() ) {
118
          if( change.wasAdded() ) {
119
            change.getAddedSubList().stream().forEach( (tab) -> {
120
              ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener );
121
            } );
122
          } else if( change.wasRemoved() ) {
123
            change.getRemoved().stream().forEach( (tab) -> {
124
              ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener );
125
            } );
126
          }
127
        }
128
129
        // Changes in the tabs may also change anyFileEditorModified property
130
        // (e.g. closed modified file)
131
        modifiedListener.changed( null, null, null );
132
      }
133
    );
134
  }
135
136
  /**
137
   * Delegates to the active file editor.
138
   *
139
   * @param <T> Event type.
140
   * @param <U> Consumer type.
141
   * @param event Event to pass to the editor.
142
   * @param consumer Consumer to pass to the editor.
143
   */
144
  public <T extends Event, U extends T> void addEventListener(
145
    final EventPattern<? super T, ? extends U> event,
146
    final Consumer<? super U> consumer ) {
147
    getActiveFileEditor().addEventListener( event, consumer );
148
  }
149
150
  /**
151
   * Delegates to the active file editor pane, and, ultimately, to its text
152
   * area.
153
   *
154
   * @param map The map of methods to events.
155
   */
156
  public void addEventListener( final InputMap<InputEvent> map ) {
157
    getActiveFileEditor().addEventListener( map );
158
  }
159
160
  /**
161
   * Remove a keyboard event listener from the active file editor.
162
   *
163
   * @param map The keyboard events to remove.
164
   */
165
  public void removeEventListener( final InputMap<InputEvent> map ) {
166
    getActiveFileEditor().removeEventListener( map );
167
  }
168
169
  /**
170
   * Allows observers to be notified when the current file editor tab changes.
171
   *
172
   * @param listener The listener to notify of tab change events.
173
   */
174
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
175
    // Observe the tab so that when a new tab is opened or selected,
176
    // a notification is kicked off.
177
    getSelectionModel().selectedItemProperty().addListener( listener );
178
  }
179
180
  /**
181
   * Allows clients to manipulate the editor content directly.
182
   *
183
   * @return The text area for the active file editor.
184
   */
185
  public StyledTextArea getEditor() {
186
    return getActiveFileEditor().getEditorPane().getEditor();
187
  }
188
189
  public FileEditorTab getActiveFileEditor() {
190
    return this.activeFileEditor.get();
191
  }
192
193
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
194
    return this.activeFileEditor.getReadOnlyProperty();
195
  }
196
197
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
198
    return this.anyFileEditorModified.getReadOnlyProperty();
199
  }
200
201
  private FileEditorTab createFileEditor( final Path path ) {
202
    final FileEditorTab tab = new FileEditorTab( path );
203
204
    tab.setOnCloseRequest( e -> {
205
      if( !canCloseEditor( tab ) ) {
206
        e.consume();
207
      }
208
    } );
209
210
    return tab;
211
  }
212
213
  /**
214
   * Called when the user selects New from the File menu.
215
   *
216
   * @return The newly added tab.
217
   */
218
  void newEditor() {
219
    final FileEditorTab tab = createFileEditor( null );
220
221
    getTabs().add( tab );
222
    getSelectionModel().select( tab );
223
  }
224
225
  void openFileDialog() {
226
    final String title = get( "Dialog.file.choose.open.title" );
227
    final FileChooser dialog = createFileChooser( title );
228
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
229
230
    if( files != null ) {
231
      openFiles( files );
232
    }
233
  }
234
235
  /**
236
   * Opens the files into new editors, unless one of those files was a
237
   * definition file. The definition file is loaded into the definition pane,
238
   * but only the first one selected (multiple definition files will result in a
239
   * warning).
240
   *
241
   * @param files The list of non-definition files that the were requested to
242
   * open.
243
   *
244
   * @return A list of files that can be opened in text editors.
245
   */
246
  private void openFiles( final List<File> files ) {
247
    final FileTypePredicate predicate
248
      = new FileTypePredicate( createExtensionFilter( "definition" ).getExtensions() );
249
250
    // The user might have opened multiple definitions files. These will
251
    // be discarded from the text editable files.
252
    final List<File> definitions
253
      = files.stream().filter( predicate ).collect( Collectors.toList() );
254
255
    // Create a modifiable list to remove any definition files that were
256
    // opened.
257
    final List<File> editors = new ArrayList<>( files );
258
259
    if( editors.size() > 0 ) {
260
      saveLastDirectory( editors.get( 0 ) );
261
    }
262
263
    editors.removeAll( definitions );
264
265
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
266
    if( editors.size() > 0 ) {
267
      openEditors( editors, 0 );
268
    }
269
270
    if( definitions.size() > 0 ) {
271
      openDefinition( definitions.get( 0 ) );
272
    }
273
  }
274
275
  private void openEditors( final List<File> files, final int activeIndex ) {
276
    final int fileTally = files.size();
277
    final List<Tab> tabs = getTabs();
278
279
    // Close single unmodified "Untitled" tab.
280
    if( tabs.size() == 1 ) {
281
      final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ));
282
283
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
284
        closeEditor( fileEditor, false );
285
      }
286
    }
287
288
    for( int i = 0; i < fileTally; i++ ) {
289
      final Path path = files.get( i ).toPath();
290
291
      FileEditorTab fileEditorTab = findEditor( path );
292
293
      // Only open new files.
294
      if( fileEditorTab == null ) {
295
        fileEditorTab = createFileEditor( path );
296
        getTabs().add( fileEditorTab );
297
      }
298
299
      // Select the first file in the list.
300
      if( i == activeIndex ) {
301
        getSelectionModel().select( fileEditorTab );
302
      }
303
    }
304
  }
305
306
  /**
307
   * Returns a property that changes when a new definition file is opened.
308
   *
309
   * @return The path to a definition file that was opened.
310
   */
311
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
312
    return getOnOpenDefinitionFile().getReadOnlyProperty();
313
  }
314
315
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
316
    return this.openDefinition;
317
  }
318
319
  /**
320
   * Called when the user has opened a definition file (using the file open
321
   * dialog box). This will replace the current set of definitions for the
322
   * active tab.
323
   *
324
   * @param definition The file to open.
325
   */
326
  private void openDefinition( final File definition ) {
327
    // TODO: Prevent reading this file twice when a new text document is opened.
328
    // (might be a matter of checking the value first).
329
    getOnOpenDefinitionFile().set( definition.toPath() );
330
  }
331
332
  boolean saveEditor( final FileEditorTab fileEditor ) {
333
    if( fileEditor == null || !fileEditor.isModified() ) {
334
      return true;
335
    }
336
337
    if( fileEditor.getPath() == null ) {
338
      getSelectionModel().select( fileEditor );
339
340
      final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) );
341
      final File file = fileChooser.showSaveDialog( getWindow() );
342
      if( file == null ) {
343
        return false;
344
      }
345
346
      saveLastDirectory( file );
347
      fileEditor.setPath( file.toPath() );
348
    }
349
350
    return fileEditor.save();
351
  }
352
353
  boolean saveAllEditors() {
354
    boolean success = true;
355
356
    for( FileEditorTab fileEditor : getAllEditors() ) {
357
      if( !saveEditor( fileEditor ) ) {
358
        success = false;
359
      }
360
    }
361
362
    return success;
363
  }
364
365
  /**
366
   * Answers whether the file has had modifications. '
367
   *
368
   * @param tab THe tab to check for modifications.
369
   *
370
   * @return false The file is unmodified.
371
   */
372
  boolean canCloseEditor( final FileEditorTab tab ) {
373
    if( !tab.isModified() ) {
374
      return true;
375
    }
376
377
    final AlertMessage message = getAlertService().createAlertMessage(
378
      Messages.get( "Alert.file.close.title" ),
379
      Messages.get( "Alert.file.close.text" ),
380
      tab.getText()
381
    );
382
383
    final Alert alert = getAlertService().createAlertConfirmation( message );
384
    final ButtonType response = alert.showAndWait().get();
385
386
    return response == YES ? saveEditor( tab ) : response == NO;
387
  }
388
389
  private AlertService getAlertService() {
390
    return this.alertService;
391
  }
392
393
  boolean closeEditor( FileEditorTab fileEditor, boolean save ) {
394
    if( fileEditor == null ) {
395
      return true;
396
    }
397
398
    final Tab tab = fileEditor;
399
400
    if( save ) {
401
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
402
      Event.fireEvent( tab, event );
403
404
      if( event.isConsumed() ) {
405
        return false;
406
      }
407
    }
408
409
    getTabs().remove( tab );
410
411
    if( tab.getOnClosed() != null ) {
412
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
413
    }
414
415
    return true;
416
  }
417
418
  boolean closeAllEditors() {
419
    final FileEditorTab[] allEditors = getAllEditors();
420
    final FileEditorTab activeEditor = getActiveFileEditor();
421
422
    // try to save active tab first because in case the user decides to cancel,
423
    // then it stays active
424
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
425
      return false;
426
    }
427
428
    // This should be called any time a tab changes.
429
    persistPreferences();
430
431
    // save modified tabs
432
    for( int i = 0; i < allEditors.length; i++ ) {
433
      final FileEditorTab fileEditor = allEditors[ i ];
434
435
      if( fileEditor == activeEditor ) {
436
        continue;
437
      }
438
439
      if( fileEditor.isModified() ) {
440
        // activate the modified tab to make its modified content visible to the user
441
        getSelectionModel().select( i );
442
443
        if( !canCloseEditor( fileEditor ) ) {
444
          return false;
445
        }
446
      }
447
    }
448
449
    // Close all tabs.
450
    for( final FileEditorTab fileEditor : allEditors ) {
451
      if( !closeEditor( fileEditor, false ) ) {
452
        return false;
453
      }
454
    }
455
456
    return getTabs().isEmpty();
457
  }
458
459
  private FileEditorTab[] getAllEditors() {
460
    final ObservableList<Tab> tabs = getTabs();
461
    final int length = tabs.size();
462
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
463
464
    for( int i = 0; i < length; i++ ) {
465
      allEditors[ i ] = (FileEditorTab)tabs.get( i );
466
    }
467
468
    return allEditors;
469
  }
470
471
  /**
472
   * Returns the file editor tab that has the given path.
473
   *
474
   * @return null No file editor tab for the given path was found.
475
   */
476
  private FileEditorTab findEditor( final Path path ) {
477
    for( final Tab tab : getTabs() ) {
478
      final FileEditorTab fileEditor = (FileEditorTab)tab;
479
480
      if( fileEditor.isPath( path ) ) {
481
        return fileEditor;
482
      }
483
    }
484
485
    return null;
486
  }
487
488
  private FileChooser createFileChooser( String title ) {
489
    final FileChooser fileChooser = new FileChooser();
490
491
    fileChooser.setTitle( title );
492
    fileChooser.getExtensionFilters().addAll(
493
      createExtensionFilters() );
494
495
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
496
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
497
498
    if( !file.isDirectory() ) {
499
      file = new File( "." );
500
    }
501
502
    fileChooser.setInitialDirectory( file );
503
    return fileChooser;
504
  }
505
506
  private List<ExtensionFilter> createExtensionFilters() {
507
    final List<ExtensionFilter> list = new ArrayList<>();
508
509
    // TODO: Return a list of all properties that match the filter prefix.
510
    // This will allow dynamic filters to be added and removed just by
511
    // updating the properties file.
512
    list.add( createExtensionFilter( "markdown" ) );
513
    list.add( createExtensionFilter( "definition" ) );
514
    list.add( createExtensionFilter( "xml" ) );
515
    list.add( createExtensionFilter( "all" ) );
516
    return list;
517
  }
518
519
  private ExtensionFilter createExtensionFilter( final String filetype ) {
520
    final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype );
521
    final String eKey = String.format( "%s.ext.%s", FILTER_EXTENSIONS, filetype );
30
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
31
import static com.scrivenvar.FileType.*;
32
import static com.scrivenvar.Messages.get;
33
import com.scrivenvar.predicates.files.FileTypePredicate;
34
import com.scrivenvar.service.Options;
35
import com.scrivenvar.service.Settings;
36
import com.scrivenvar.service.events.AlertMessage;
37
import com.scrivenvar.service.events.AlertService;
38
import static com.scrivenvar.service.events.AlertService.NO;
39
import static com.scrivenvar.service.events.AlertService.YES;
40
import com.scrivenvar.util.Utils;
41
import java.io.File;
42
import java.nio.file.Path;
43
import java.util.ArrayList;
44
import java.util.List;
45
import java.util.function.Consumer;
46
import java.util.prefs.Preferences;
47
import java.util.stream.Collectors;
48
import javafx.beans.property.ReadOnlyBooleanProperty;
49
import javafx.beans.property.ReadOnlyBooleanWrapper;
50
import javafx.beans.property.ReadOnlyObjectProperty;
51
import javafx.beans.property.ReadOnlyObjectWrapper;
52
import javafx.beans.value.ChangeListener;
53
import javafx.beans.value.ObservableValue;
54
import javafx.collections.ListChangeListener;
55
import javafx.collections.ObservableList;
56
import javafx.event.Event;
57
import javafx.scene.Node;
58
import javafx.scene.control.Alert;
59
import javafx.scene.control.ButtonType;
60
import javafx.scene.control.Tab;
61
import javafx.scene.control.TabPane;
62
import javafx.scene.control.TabPane.TabClosingPolicy;
63
import javafx.scene.input.InputEvent;
64
import javafx.stage.FileChooser;
65
import javafx.stage.FileChooser.ExtensionFilter;
66
import javafx.stage.Window;
67
import org.fxmisc.richtext.StyledTextArea;
68
import org.fxmisc.wellbehaved.event.EventPattern;
69
import org.fxmisc.wellbehaved.event.InputMap;
70
71
/**
72
 * Tab pane for file editors.
73
 *
74
 * @author Karl Tauber and White Magic Software, Ltd.
75
 */
76
public final class FileEditorTabPane extends TabPane {
77
78
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter";
79
80
  private final Options options = Services.load( Options.class );
81
  private final Settings settings = Services.load( Settings.class );
82
  private final AlertService alertService = Services.load( AlertService.class );
83
84
  private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
86
  private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
87
88
  /**
89
   * Constructs a new file editor tab pane.
90
   */
91
  public FileEditorTabPane() {
92
    final ObservableList<Tab> tabs = getTabs();
93
94
    setFocusTraversable( false );
95
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
96
97
    addTabSelectionListener(
98
      (ObservableValue<? extends Tab> tabPane,
99
        final Tab oldTab, final Tab newTab) -> {
100
101
        if( newTab != null ) {
102
          activeFileEditor.set( (FileEditorTab)newTab );
103
        }
104
      }
105
    );
106
107
    final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> {
108
      for( final Tab tab : tabs ) {
109
        if( ((FileEditorTab)tab).isModified() ) {
110
          this.anyFileEditorModified.set( true );
111
          break;
112
        }
113
      }
114
    };
115
116
    tabs.addListener(
117
      (ListChangeListener<Tab>)change -> {
118
        while( change.next() ) {
119
          if( change.wasAdded() ) {
120
            change.getAddedSubList().stream().forEach( (tab) -> {
121
              ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener );
122
            } );
123
          } else if( change.wasRemoved() ) {
124
            change.getRemoved().stream().forEach( (tab) -> {
125
              ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener );
126
            } );
127
          }
128
        }
129
130
        // Changes in the tabs may also change anyFileEditorModified property
131
        // (e.g. closed modified file)
132
        modifiedListener.changed( null, null, null );
133
      }
134
    );
135
  }
136
137
  /**
138
   * Delegates to the active file editor.
139
   *
140
   * @param <T> Event type.
141
   * @param <U> Consumer type.
142
   * @param event Event to pass to the editor.
143
   * @param consumer Consumer to pass to the editor.
144
   */
145
  public <T extends Event, U extends T> void addEventListener(
146
    final EventPattern<? super T, ? extends U> event,
147
    final Consumer<? super U> consumer ) {
148
    getActiveFileEditor().addEventListener( event, consumer );
149
  }
150
151
  /**
152
   * Delegates to the active file editor pane, and, ultimately, to its text
153
   * area.
154
   *
155
   * @param map The map of methods to events.
156
   */
157
  public void addEventListener( final InputMap<InputEvent> map ) {
158
    getActiveFileEditor().addEventListener( map );
159
  }
160
161
  /**
162
   * Remove a keyboard event listener from the active file editor.
163
   *
164
   * @param map The keyboard events to remove.
165
   */
166
  public void removeEventListener( final InputMap<InputEvent> map ) {
167
    getActiveFileEditor().removeEventListener( map );
168
  }
169
170
  /**
171
   * Allows observers to be notified when the current file editor tab changes.
172
   *
173
   * @param listener The listener to notify of tab change events.
174
   */
175
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
176
    // Observe the tab so that when a new tab is opened or selected,
177
    // a notification is kicked off.
178
    getSelectionModel().selectedItemProperty().addListener( listener );
179
  }
180
181
  /**
182
   * Allows clients to manipulate the editor content directly.
183
   *
184
   * @return The text area for the active file editor.
185
   */
186
  public StyledTextArea getEditor() {
187
    return getActiveFileEditor().getEditorPane().getEditor();
188
  }
189
190
  public FileEditorTab getActiveFileEditor() {
191
    return this.activeFileEditor.get();
192
  }
193
194
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
195
    return this.activeFileEditor.getReadOnlyProperty();
196
  }
197
198
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
199
    return this.anyFileEditorModified.getReadOnlyProperty();
200
  }
201
202
  private FileEditorTab createFileEditor( final Path path ) {
203
    final FileEditorTab tab = new FileEditorTab( path );
204
205
    tab.setOnCloseRequest( e -> {
206
      if( !canCloseEditor( tab ) ) {
207
        e.consume();
208
      }
209
    } );
210
211
    return tab;
212
  }
213
214
  /**
215
   * Called when the user selects New from the File menu.
216
   *
217
   * @return The newly added tab.
218
   */
219
  void newEditor() {
220
    final FileEditorTab tab = createFileEditor( null );
221
222
    getTabs().add( tab );
223
    getSelectionModel().select( tab );
224
  }
225
226
  void openFileDialog() {
227
    final String title = get( "Dialog.file.choose.open.title" );
228
    final FileChooser dialog = createFileChooser( title );
229
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
230
231
    if( files != null ) {
232
      openFiles( files );
233
    }
234
  }
235
236
  /**
237
   * Opens the files into new editors, unless one of those files was a
238
   * definition file. The definition file is loaded into the definition pane,
239
   * but only the first one selected (multiple definition files will result in a
240
   * warning).
241
   *
242
   * @param files The list of non-definition files that the were requested to
243
   * open.
244
   *
245
   * @return A list of files that can be opened in text editors.
246
   */
247
  private void openFiles( final List<File> files ) {
248
    final FileTypePredicate predicate
249
      = new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() );
250
251
    // The user might have opened multiple definitions files. These will
252
    // be discarded from the text editable files.
253
    final List<File> definitions
254
      = files.stream().filter( predicate ).collect( Collectors.toList() );
255
256
    // Create a modifiable list to remove any definition files that were
257
    // opened.
258
    final List<File> editors = new ArrayList<>( files );
259
260
    if( editors.size() > 0 ) {
261
      saveLastDirectory( editors.get( 0 ) );
262
    }
263
264
    editors.removeAll( definitions );
265
266
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
267
    if( editors.size() > 0 ) {
268
      openEditors( editors, 0 );
269
    }
270
271
    if( definitions.size() > 0 ) {
272
      openDefinition( definitions.get( 0 ) );
273
    }
274
  }
275
276
  private void openEditors( final List<File> files, final int activeIndex ) {
277
    final int fileTally = files.size();
278
    final List<Tab> tabs = getTabs();
279
280
    // Close single unmodified "Untitled" tab.
281
    if( tabs.size() == 1 ) {
282
      final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ));
283
284
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
285
        closeEditor( fileEditor, false );
286
      }
287
    }
288
289
    for( int i = 0; i < fileTally; i++ ) {
290
      final Path path = files.get( i ).toPath();
291
292
      FileEditorTab fileEditorTab = findEditor( path );
293
294
      // Only open new files.
295
      if( fileEditorTab == null ) {
296
        fileEditorTab = createFileEditor( path );
297
        getTabs().add( fileEditorTab );
298
      }
299
300
      // Select the first file in the list.
301
      if( i == activeIndex ) {
302
        getSelectionModel().select( fileEditorTab );
303
      }
304
    }
305
  }
306
307
  /**
308
   * Returns a property that changes when a new definition file is opened.
309
   *
310
   * @return The path to a definition file that was opened.
311
   */
312
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
313
    return getOnOpenDefinitionFile().getReadOnlyProperty();
314
  }
315
316
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
317
    return this.openDefinition;
318
  }
319
320
  /**
321
   * Called when the user has opened a definition file (using the file open
322
   * dialog box). This will replace the current set of definitions for the
323
   * active tab.
324
   *
325
   * @param definition The file to open.
326
   */
327
  private void openDefinition( final File definition ) {
328
    // TODO: Prevent reading this file twice when a new text document is opened.
329
    // (might be a matter of checking the value first).
330
    getOnOpenDefinitionFile().set( definition.toPath() );
331
  }
332
333
  boolean saveEditor( final FileEditorTab fileEditor ) {
334
    if( fileEditor == null || !fileEditor.isModified() ) {
335
      return true;
336
    }
337
338
    if( fileEditor.getPath() == null ) {
339
      getSelectionModel().select( fileEditor );
340
341
      final FileChooser fileChooser = createFileChooser( Messages.get( "Dialog.file.choose.save.title" ) );
342
      final File file = fileChooser.showSaveDialog( getWindow() );
343
      if( file == null ) {
344
        return false;
345
      }
346
347
      saveLastDirectory( file );
348
      fileEditor.setPath( file.toPath() );
349
    }
350
351
    return fileEditor.save();
352
  }
353
354
  boolean saveAllEditors() {
355
    boolean success = true;
356
357
    for( FileEditorTab fileEditor : getAllEditors() ) {
358
      if( !saveEditor( fileEditor ) ) {
359
        success = false;
360
      }
361
    }
362
363
    return success;
364
  }
365
366
  /**
367
   * Answers whether the file has had modifications. '
368
   *
369
   * @param tab THe tab to check for modifications.
370
   *
371
   * @return false The file is unmodified.
372
   */
373
  boolean canCloseEditor( final FileEditorTab tab ) {
374
    if( !tab.isModified() ) {
375
      return true;
376
    }
377
378
    final AlertMessage message = getAlertService().createAlertMessage(
379
      Messages.get( "Alert.file.close.title" ),
380
      Messages.get( "Alert.file.close.text" ),
381
      tab.getText()
382
    );
383
384
    final Alert alert = getAlertService().createAlertConfirmation( message );
385
    final ButtonType response = alert.showAndWait().get();
386
387
    return response == YES ? saveEditor( tab ) : response == NO;
388
  }
389
390
  private AlertService getAlertService() {
391
    return this.alertService;
392
  }
393
394
  boolean closeEditor( FileEditorTab fileEditor, boolean save ) {
395
    if( fileEditor == null ) {
396
      return true;
397
    }
398
399
    final Tab tab = fileEditor;
400
401
    if( save ) {
402
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
403
      Event.fireEvent( tab, event );
404
405
      if( event.isConsumed() ) {
406
        return false;
407
      }
408
    }
409
410
    getTabs().remove( tab );
411
412
    if( tab.getOnClosed() != null ) {
413
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
414
    }
415
416
    return true;
417
  }
418
419
  boolean closeAllEditors() {
420
    final FileEditorTab[] allEditors = getAllEditors();
421
    final FileEditorTab activeEditor = getActiveFileEditor();
422
423
    // try to save active tab first because in case the user decides to cancel,
424
    // then it stays active
425
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
426
      return false;
427
    }
428
429
    // This should be called any time a tab changes.
430
    persistPreferences();
431
432
    // save modified tabs
433
    for( int i = 0; i < allEditors.length; i++ ) {
434
      final FileEditorTab fileEditor = allEditors[ i ];
435
436
      if( fileEditor == activeEditor ) {
437
        continue;
438
      }
439
440
      if( fileEditor.isModified() ) {
441
        // activate the modified tab to make its modified content visible to the user
442
        getSelectionModel().select( i );
443
444
        if( !canCloseEditor( fileEditor ) ) {
445
          return false;
446
        }
447
      }
448
    }
449
450
    // Close all tabs.
451
    for( final FileEditorTab fileEditor : allEditors ) {
452
      if( !closeEditor( fileEditor, false ) ) {
453
        return false;
454
      }
455
    }
456
457
    return getTabs().isEmpty();
458
  }
459
460
  private FileEditorTab[] getAllEditors() {
461
    final ObservableList<Tab> tabs = getTabs();
462
    final int length = tabs.size();
463
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
464
465
    for( int i = 0; i < length; i++ ) {
466
      allEditors[ i ] = (FileEditorTab)tabs.get( i );
467
    }
468
469
    return allEditors;
470
  }
471
472
  /**
473
   * Returns the file editor tab that has the given path.
474
   *
475
   * @return null No file editor tab for the given path was found.
476
   */
477
  private FileEditorTab findEditor( final Path path ) {
478
    for( final Tab tab : getTabs() ) {
479
      final FileEditorTab fileEditor = (FileEditorTab)tab;
480
481
      if( fileEditor.isPath( path ) ) {
482
        return fileEditor;
483
      }
484
    }
485
486
    return null;
487
  }
488
489
  private FileChooser createFileChooser( String title ) {
490
    final FileChooser fileChooser = new FileChooser();
491
492
    fileChooser.setTitle( title );
493
    fileChooser.getExtensionFilters().addAll(
494
      createExtensionFilters() );
495
496
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
497
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
498
499
    if( !file.isDirectory() ) {
500
      file = new File( "." );
501
    }
502
503
    fileChooser.setInitialDirectory( file );
504
    return fileChooser;
505
  }
506
507
  private List<ExtensionFilter> createExtensionFilters() {
508
    final List<ExtensionFilter> list = new ArrayList<>();
509
510
    // TODO: Return a list of all properties that match the filter prefix.
511
    // This will allow dynamic filters to be added and removed just by
512
    // updating the properties file.
513
    list.add( createExtensionFilter( MARKDOWN ) );
514
    list.add( createExtensionFilter( DEFINITION ) );
515
    list.add( createExtensionFilter( XML ) );
516
    list.add( createExtensionFilter( ALL ) );
517
    return list;
518
  }
519
520
  /**
521
   * Returns a filter for file name extensions recognized by the application
522
   * that can be opened by the user.
523
   *
524
   * @param filetype Used to find the globbing pattern for extensions.
525
   *
526
   * @return A filename filter suitable for use by a FileDialog instance.
527
   */
528
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
529
    final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype );
530
    final String eKey = String.format("%s.%s", GLOB_PREFIX_FILE, filetype );
522531
523532
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
A src/main/java/com/scrivenvar/FileType.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
/**
31
 * Represents different file type classifications. These are high-level mappings
32
 * that correspond to the list of glob patterns found within
33
 * settings.properties.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public enum FileType {
38
39
  ALL( "all" ),
40
  RMARKDOWN( "rmarkdown" ),
41
  RXML( "rxml" ),
42
  MARKDOWN( "markdown" ),
43
  DEFINITION( "definition" ),
44
  XML( "xml" ),
45
  JSON( "json" ),
46
  TOML( "toml" ),
47
  YAML( "yaml" ),
48
  PROPERTIES( "properties" );
49
50
  private final String type;
51
52
  private FileType( final String type ) {
53
    this.type = type;
54
  }
55
56
  /**
57
   * Returns the file type that corresponds to the given string.
58
   *
59
   * @param type The string to compare against this enumeration of file types.
60
   *
61
   * @return The corresponding File Type for the given string.
62
   *
63
   * @throws IllegalArgumentException Type not found.
64
   */
65
  public static FileType from( final String type ) {
66
    for( final FileType fileType : FileType.values() ) {
67
      if( fileType.isType( type ) ) {
68
        return fileType;
69
      }
70
    }
71
72
    throw new IllegalArgumentException( type );
73
  }
74
75
  /**
76
   * Answers whether this file type matches the given string, case insensitive
77
   * comparison.
78
   *
79
   * @param type Presumably a file name extension to check against.
80
   *
81
   * @return true The given extension corresponds to this enumerated type.
82
   */
83
  public boolean isType( final String type ) {
84
    return getType().equalsIgnoreCase( type );
85
  }
86
87
  private String getType() {
88
    return this.type;
89
  }
90
91
  /**
92
   * Returns the lowercase version of the file name extension.
93
   *
94
   * @return The file name, in lower case.
95
   */
96
  @Override
97
  public String toString() {
98
    return getType();
99
  }
100
}
1101
M src/main/java/com/scrivenvar/MainWindow.java
2828
package com.scrivenvar;
2929
30
import static com.scrivenvar.Constants.FILE_LOGO_32;
31
import static com.scrivenvar.Constants.PREFS_DEFINITION_SOURCE;
32
import static com.scrivenvar.Constants.STYLESHEET_SCENE;
33
import static com.scrivenvar.Messages.get;
34
import com.scrivenvar.definition.DefinitionFactory;
35
import com.scrivenvar.definition.DefinitionPane;
36
import com.scrivenvar.definition.DefinitionSource;
37
import com.scrivenvar.definition.EmptyDefinitionSource;
38
import com.scrivenvar.editors.EditorPane;
39
import com.scrivenvar.editors.VariableNameInjector;
40
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
41
import com.scrivenvar.preview.HTMLPreviewPane;
42
import com.scrivenvar.processors.CaretReplacementProcessor;
43
import com.scrivenvar.processors.HTMLPreviewProcessor;
44
import com.scrivenvar.processors.MarkdownProcessor;
45
import com.scrivenvar.processors.Processor;
46
import com.scrivenvar.processors.VariableProcessor;
47
import com.scrivenvar.processors.XMLCaretInsertionProcessor;
48
import com.scrivenvar.processors.XMLProcessor;
49
import com.scrivenvar.service.Options;
50
import com.scrivenvar.util.Action;
51
import com.scrivenvar.util.ActionUtils;
52
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_DEFINITION;
53
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_EDITOR;
54
import static com.scrivenvar.util.StageState.K_PANE_SPLIT_PREVIEW;
55
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.BOLD;
56
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.CODE;
57
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_ALT;
58
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FILE_CODE_ALT;
59
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FLOPPY_ALT;
60
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.FOLDER_OPEN_ALT;
61
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.HEADER;
62
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.ITALIC;
63
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LINK;
64
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_OL;
65
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.LIST_UL;
66
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.PICTURE_ALT;
67
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.QUOTE_LEFT;
68
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.REPEAT;
69
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.STRIKETHROUGH;
70
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.UNDO;
71
import java.net.MalformedURLException;
72
import java.nio.file.Path;
73
import java.util.Map;
74
import java.util.function.Function;
75
import java.util.prefs.Preferences;
76
import javafx.beans.binding.Bindings;
77
import javafx.beans.binding.BooleanBinding;
78
import javafx.beans.property.BooleanProperty;
79
import javafx.beans.property.SimpleBooleanProperty;
80
import javafx.beans.value.ObservableBooleanValue;
81
import javafx.beans.value.ObservableValue;
82
import javafx.collections.ListChangeListener.Change;
83
import javafx.collections.ObservableList;
84
import static javafx.event.Event.fireEvent;
85
import javafx.scene.Node;
86
import javafx.scene.Scene;
87
import javafx.scene.control.Alert;
88
import javafx.scene.control.Alert.AlertType;
89
import javafx.scene.control.Menu;
90
import javafx.scene.control.MenuBar;
91
import javafx.scene.control.SplitPane;
92
import javafx.scene.control.Tab;
93
import javafx.scene.control.ToolBar;
94
import javafx.scene.control.TreeView;
95
import javafx.scene.image.Image;
96
import javafx.scene.image.ImageView;
97
import static javafx.scene.input.KeyCode.ESCAPE;
98
import javafx.scene.input.KeyEvent;
99
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
100
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
101
import javafx.scene.layout.BorderPane;
102
import javafx.scene.layout.VBox;
103
import javafx.stage.Window;
104
import javafx.stage.WindowEvent;
105
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
106
107
/**
108
 * Main window containing a tab pane in the center for file editors.
109
 *
110
 * @author Karl Tauber and White Magic Software, Ltd.
111
 */
112
public class MainWindow {
113
114
  private final Options options = Services.load( Options.class );
115
116
  private Scene scene;
117
  private MenuBar menuBar;
118
119
  private DefinitionPane definitionPane;
120
  private FileEditorTabPane fileEditorPane;
121
  private HTMLPreviewPane previewPane;
122
123
  private DefinitionSource definitionSource;
124
125
  public MainWindow() {
126
    initLayout();
127
    initOpenDefinitionListener();
128
    initTabAddedListener();
129
    initTabChangedListener();
130
    initPreferences();
131
  }
132
133
  /**
134
   * Listen for file editor tab pane to receive an open definition source event.
135
   */
136
  private void initOpenDefinitionListener() {
137
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
138
      (ObservableValue<? extends Path> definitionFile,
139
        final Path oldPath, final Path newPath) -> {
140
        openDefinition( newPath );
141
        refreshSelectedTab( getActiveFileEditor() );
142
      } );
143
  }
144
145
  /**
146
   * When tabs are added, hook the various change listeners onto the new tab so
147
   * that the preview pane refreshes as necessary.
148
   */
149
  private void initTabAddedListener() {
150
    final FileEditorTabPane editorPane = getFileEditorPane();
151
152
    // Make sure the text processor kicks off when new files are opened.
153
    final ObservableList<Tab> tabs = editorPane.getTabs();
154
155
    // Update the preview pane on tab changes.
156
    tabs.addListener(
157
      (final Change<? extends Tab> change) -> {
158
        while( change.next() ) {
159
          if( change.wasAdded() ) {
160
            // Multiple tabs can be added simultaneously.
161
            for( final Tab newTab : change.getAddedSubList() ) {
162
              final FileEditorTab tab = (FileEditorTab)newTab;
163
164
              initTextChangeListener( tab );
165
              initCaretParagraphListener( tab );
166
              initVariableNameInjector( tab );
167
            }
168
          }
169
        }
170
      }
171
    );
172
  }
173
174
  /**
175
   * Reloads the preferences from the previous load.
176
   */
177
  private void initPreferences() {
178
    getFileEditorPane().restorePreferences();
179
    restoreDefinitionSource();
180
  }
181
182
  /**
183
   * Listen for new tab selection events.
184
   */
185
  private void initTabChangedListener() {
186
    final FileEditorTabPane editorPane = getFileEditorPane();
187
188
    // Update the preview pane changing tabs.
189
    editorPane.addTabSelectionListener(
190
      (ObservableValue<? extends Tab> tabPane,
191
        final Tab oldTab, final Tab newTab) -> {
192
193
        // If there was no old tab, then this is a first time load, which
194
        // can be ignored.
195
        if( oldTab != null ) {
196
          if( newTab == null ) {
197
            closeRemainingTab();
198
          } else {
199
            // Update the preview with the edited text.
200
            refreshSelectedTab( (FileEditorTab)newTab );
201
          }
202
        }
203
      }
204
    );
205
  }
206
207
  private void initTextChangeListener( final FileEditorTab tab ) {
208
    tab.addTextChangeListener(
209
      (ObservableValue<? extends String> editor,
210
        final String oldValue, final String newValue) -> {
211
        refreshSelectedTab( tab );
212
      }
213
    );
214
  }
215
216
  private void initCaretParagraphListener( final FileEditorTab tab ) {
217
    tab.addCaretParagraphListener(
218
      (ObservableValue<? extends Integer> editor,
219
        final Integer oldValue, final Integer newValue) -> {
220
        refreshSelectedTab( tab );
221
      }
222
    );
223
  }
224
225
  private void initVariableNameInjector( final FileEditorTab tab ) {
226
    VariableNameInjector.listen( tab, getDefinitionPane() );
227
  }
228
  
229
  /**
230
   * Called whenever the preview pane becomes out of sync with the file editor
231
   * tab. This can be called when the text changes, the caret paragraph changes,
232
   * or the file tab changes.
233
   *
234
   * @param tab The file editor tab that has been changed in some fashion.
235
   */
236
  private void refreshSelectedTab( final FileEditorTab tab ) {
237
    final Path path = tab.getPath();
238
239
    final HTMLPreviewPane preview = getPreviewPane();
240
    preview.setPath( tab.getPath() );
241
242
    final Processor<String> hpp = new HTMLPreviewProcessor( preview );
243
    final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
244
    final Processor<String> mp = new MarkdownProcessor( mcrp );
245
//    final Processor<String> mcip = new MarkdownCaretInsertionProcessor( mp, tab.getCaretPosition() );
246
    final Processor<String> xmlp = new XMLProcessor( mp, tab.getPath() );
247
    final Processor<String> xcip = new XMLCaretInsertionProcessor( xmlp, tab.getCaretPosition() );
248
    final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
249
250
    vp.processChain( tab.getEditorText() );
251
  }
252
253
  /**
254
   * Returns the variable map of interpolated definitions.
255
   *
256
   * @return A map to help dereference variables.
257
   */
258
  private Map<String, String> getResolvedMap() {
259
    return getDefinitionSource().getResolvedMap();
260
  }
261
262
  /**
263
   * Returns the root node for the hierarchical definition source.
264
   *
265
   * @return Data to display in the definition pane.
266
   */
267
  private TreeView<String> getTreeView() {
268
    try {
269
      return getDefinitionSource().asTreeView();
270
    } catch( Exception e ) {
271
      alert( e );
272
    }
273
274
    return new TreeView<>();
275
  }
276
277
  private void openDefinition( final Path path ) {
278
    openDefinition( path.toString() );
279
  }
280
281
  private void openDefinition( final String path ) {
282
    try {
283
      final DefinitionSource ds = createDefinitionSource( path );
284
      setDefinitionSource( ds );
285
      storeDefinitionSource();
286
287
      getDefinitionPane().setRoot( ds.asTreeView() );
288
    } catch( Exception e ) {
289
      alert( e );
290
    }
291
  }
292
293
  private void restoreDefinitionSource() {
294
    final Preferences preferences = getPreferences();
295
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
296
297
    if( source != null ) {
298
      openDefinition( source );
299
    }
300
  }
301
302
  private void storeDefinitionSource() {
303
    final Preferences preferences = getPreferences();
304
    final DefinitionSource ds = getDefinitionSource();
305
306
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
307
  }
308
309
  /**
310
   * Called when the last open tab is closed. This clears out the preview pane
311
   * and the definition pane.
312
   */
313
  private void closeRemainingTab() {
314
    getPreviewPane().clear();
315
    getDefinitionPane().clear();
316
  }
317
318
  /**
319
   * Called when an exception occurs that warrants the user's attention.
320
   *
321
   * @param e The exception with a message that the user should know about.
322
   */
323
  private void alert( final Exception e ) {
324
    // TODO: Raise a notice.
325
  }
326
327
  //---- File actions -------------------------------------------------------
328
  private void fileNew() {
329
    getFileEditorPane().newEditor();
330
  }
331
332
  private void fileOpen() {
333
    getFileEditorPane().openFileDialog();
334
  }
335
336
  private void fileClose() {
337
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
338
  }
339
340
  private void fileCloseAll() {
341
    getFileEditorPane().closeAllEditors();
342
  }
343
344
  private void fileSave() {
345
    getFileEditorPane().saveEditor( getActiveFileEditor() );
346
  }
347
348
  private void fileSaveAll() {
349
    getFileEditorPane().saveAllEditors();
350
  }
351
352
  private void fileExit() {
353
    final Window window = getWindow();
354
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
355
  }
356
357
  //---- Help actions -------------------------------------------------------
358
  private void helpAbout() {
359
    Alert alert = new Alert( AlertType.INFORMATION );
360
    alert.setTitle( get( "Dialog.about.title" ) );
361
    alert.setHeaderText( get( "Dialog.about.header" ) );
362
    alert.setContentText( get( "Dialog.about.content" ) );
363
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
364
    alert.initOwner( getWindow() );
365
366
    alert.showAndWait();
367
  }
368
369
  //---- Convenience accessors ----------------------------------------------
370
  private float getFloat( final String key, final float defaultValue ) {
371
    return getPreferences().getFloat( key, defaultValue );
372
  }
373
374
  private Preferences getPreferences() {
375
    return getOptions().getState();
376
  }
377
378
  private Window getWindow() {
379
    return getScene().getWindow();
380
  }
381
382
  private MarkdownEditorPane getActiveEditor() {
383
    final EditorPane pane = getActiveFileEditor().getEditorPane();
384
385
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
386
  }
387
388
  private FileEditorTab getActiveFileEditor() {
389
    return getFileEditorPane().getActiveFileEditor();
390
  }
391
392
  //---- Member accessors ---------------------------------------------------
393
  public Scene getScene() {
394
    return this.scene;
395
  }
396
397
  private void setScene( Scene scene ) {
398
    this.scene = scene;
399
  }
400
401
  private FileEditorTabPane getFileEditorPane() {
402
    if( this.fileEditorPane == null ) {
403
      this.fileEditorPane = createFileEditorPane();
404
    }
405
406
    return this.fileEditorPane;
407
  }
408
409
  private HTMLPreviewPane getPreviewPane() {
410
    if( this.previewPane == null ) {
411
      this.previewPane = createPreviewPane();
412
    }
413
414
    return this.previewPane;
415
  }
416
417
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
418
    this.definitionSource = definitionSource;
419
  }
420
421
  private DefinitionSource getDefinitionSource() {
422
    if( this.definitionSource == null ) {
423
      this.definitionSource = new EmptyDefinitionSource();
424
    }
425
426
    return this.definitionSource;
427
  }
428
429
  private DefinitionPane getDefinitionPane() {
430
    if( this.definitionPane == null ) {
431
      this.definitionPane = createDefinitionPane();
432
    }
433
434
    return this.definitionPane;
435
  }
436
437
  private Options getOptions() {
438
    return this.options;
439
  }
440
441
  public MenuBar getMenuBar() {
442
    return this.menuBar;
443
  }
444
445
  public void setMenuBar( MenuBar menuBar ) {
446
    this.menuBar = menuBar;
447
  }
448
449
  //---- Member creators ----------------------------------------------------
30
import static com.scrivenvar.Constants.*;
31
import static com.scrivenvar.Messages.get;
32
import com.scrivenvar.definition.DefinitionFactory;
33
import com.scrivenvar.definition.DefinitionPane;
34
import com.scrivenvar.definition.DefinitionSource;
35
import com.scrivenvar.definition.EmptyDefinitionSource;
36
import com.scrivenvar.editors.EditorPane;
37
import com.scrivenvar.editors.VariableNameInjector;
38
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
39
import com.scrivenvar.preview.HTMLPreviewPane;
40
import com.scrivenvar.processors.Processor;
41
import com.scrivenvar.processors.ProcessorFactory;
42
import com.scrivenvar.service.Options;
43
import com.scrivenvar.service.Snitch;
44
import com.scrivenvar.util.Action;
45
import com.scrivenvar.util.ActionUtils;
46
import static com.scrivenvar.util.StageState.*;
47
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
48
import java.net.MalformedURLException;
49
import java.nio.file.Path;
50
import java.util.HashMap;
51
import java.util.Map;
52
import java.util.Observable;
53
import java.util.Observer;
54
import java.util.function.Function;
55
import java.util.prefs.Preferences;
56
import javafx.application.Platform;
57
import javafx.beans.binding.Bindings;
58
import javafx.beans.binding.BooleanBinding;
59
import javafx.beans.property.BooleanProperty;
60
import javafx.beans.property.SimpleBooleanProperty;
61
import javafx.beans.value.ObservableBooleanValue;
62
import javafx.beans.value.ObservableValue;
63
import javafx.collections.ListChangeListener.Change;
64
import javafx.collections.ObservableList;
65
import static javafx.event.Event.fireEvent;
66
import javafx.scene.Node;
67
import javafx.scene.Scene;
68
import javafx.scene.control.Alert;
69
import javafx.scene.control.Alert.AlertType;
70
import javafx.scene.control.Menu;
71
import javafx.scene.control.MenuBar;
72
import javafx.scene.control.SplitPane;
73
import javafx.scene.control.Tab;
74
import javafx.scene.control.ToolBar;
75
import javafx.scene.control.TreeView;
76
import javafx.scene.image.Image;
77
import javafx.scene.image.ImageView;
78
import static javafx.scene.input.KeyCode.ESCAPE;
79
import javafx.scene.input.KeyEvent;
80
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
81
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
82
import javafx.scene.layout.BorderPane;
83
import javafx.scene.layout.VBox;
84
import javafx.stage.Window;
85
import javafx.stage.WindowEvent;
86
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
87
88
/**
89
 * Main window containing a tab pane in the center for file editors.
90
 *
91
 * @author Karl Tauber and White Magic Software, Ltd.
92
 */
93
public class MainWindow implements Observer {
94
95
  private final Options options = Services.load( Options.class );
96
  private final Snitch snitch = Services.load( Snitch.class );
97
98
  private Scene scene;
99
  private MenuBar menuBar;
100
101
  private DefinitionSource definitionSource;
102
  private DefinitionPane definitionPane;
103
  private FileEditorTabPane fileEditorPane;
104
  private HTMLPreviewPane previewPane;
105
106
  /**
107
   * Prevent re-instantiation processing classes.
108
   */
109
  private Map<FileEditorTab, Processor<String>> processors;
110
  private ProcessorFactory processorFactory;
111
  
112
113
  public MainWindow() {
114
    initLayout();
115
    initOpenDefinitionListener();
116
    initTabAddedListener();
117
    initTabChangedListener();
118
    initPreferences();
119
    initWatchDog();
120
  }
121
122
  /**
123
   * Listen for file editor tab pane to receive an open definition source event.
124
   */
125
  private void initOpenDefinitionListener() {
126
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
127
      (ObservableValue<? extends Path> definitionFile,
128
        final Path oldPath, final Path newPath) -> {
129
        openDefinition( newPath );
130
        refreshSelectedTab( getActiveFileEditor() );
131
      } );
132
  }
133
134
  /**
135
   * When tabs are added, hook the various change listeners onto the new tab so
136
   * that the preview pane refreshes as necessary.
137
   */
138
  private void initTabAddedListener() {
139
    final FileEditorTabPane editorPane = getFileEditorPane();
140
141
    // Make sure the text processor kicks off when new files are opened.
142
    final ObservableList<Tab> tabs = editorPane.getTabs();
143
144
    // Update the preview pane on tab changes.
145
    tabs.addListener(
146
      (final Change<? extends Tab> change) -> {
147
        while( change.next() ) {
148
          if( change.wasAdded() ) {
149
            // Multiple tabs can be added simultaneously.
150
            for( final Tab newTab : change.getAddedSubList() ) {
151
              final FileEditorTab tab = (FileEditorTab)newTab;
152
153
              initTextChangeListener( tab );
154
              initCaretParagraphListener( tab );
155
              initVariableNameInjector( tab );
156
            }
157
          }
158
        }
159
      }
160
    );
161
  }
162
163
  /**
164
   * Reloads the preferences from the previous load.
165
   */
166
  private void initPreferences() {
167
    getFileEditorPane().restorePreferences();
168
    restoreDefinitionSource();
169
  }
170
171
  /**
172
   * Listen for new tab selection events.
173
   */
174
  private void initTabChangedListener() {
175
    final FileEditorTabPane editorPane = getFileEditorPane();
176
177
    // Update the preview pane changing tabs.
178
    editorPane.addTabSelectionListener(
179
      (ObservableValue<? extends Tab> tabPane,
180
        final Tab oldTab, final Tab newTab) -> {
181
182
        // If there was no old tab, then this is a first time load, which
183
        // can be ignored.
184
        if( oldTab != null ) {
185
          if( newTab == null ) {
186
            closeRemainingTab();
187
          } else {
188
            // Update the preview with the edited text.
189
            refreshSelectedTab( (FileEditorTab)newTab );
190
          }
191
        }
192
      }
193
    );
194
  }
195
196
  private void initTextChangeListener( final FileEditorTab tab ) {
197
    tab.addTextChangeListener(
198
      (ObservableValue<? extends String> editor,
199
        final String oldValue, final String newValue) -> {
200
        refreshSelectedTab( tab );
201
      }
202
    );
203
  }
204
205
  private void initCaretParagraphListener( final FileEditorTab tab ) {
206
    tab.addCaretParagraphListener(
207
      (ObservableValue<? extends Integer> editor,
208
        final Integer oldValue, final Integer newValue) -> {
209
        refreshSelectedTab( tab );
210
      }
211
    );
212
  }
213
214
  private void initVariableNameInjector( final FileEditorTab tab ) {
215
    VariableNameInjector.listen( tab, getDefinitionPane() );
216
  }
217
218
  private void initWatchDog() {
219
    getSnitch().addObserver( this );
220
  }
221
222
  /**
223
   * Called whenever the preview pane becomes out of sync with the file editor
224
   * tab. This can be called when the text changes, the caret paragraph changes,
225
   * or the file tab changes.
226
   *
227
   * @param tab The file editor tab that has been changed in some fashion.
228
   */
229
  private void refreshSelectedTab( final FileEditorTab tab ) {
230
    getPreviewPane().setPath( tab.getPath() );
231
232
    Processor<String> processor = getProcessors().get( tab );
233
234
    if( processor == null ) {
235
      processor = createProcessor( tab );
236
      getProcessors().put( tab, processor );
237
    }
238
239
    processor.processChain( tab.getEditorText() );
240
  }
241
242
  /**
243
   * Returns the variable map of interpolated definitions.
244
   *
245
   * @return A map to help dereference variables.
246
   */
247
  private Map<String, String> getResolvedMap() {
248
    return getDefinitionSource().getResolvedMap();
249
  }
250
251
  /**
252
   * Returns the root node for the hierarchical definition source.
253
   *
254
   * @return Data to display in the definition pane.
255
   */
256
  private TreeView<String> getTreeView() {
257
    try {
258
      return getDefinitionSource().asTreeView();
259
    } catch( Exception e ) {
260
      alert( e );
261
    }
262
263
    return new TreeView<>();
264
  }
265
266
  private void openDefinition( final Path path ) {
267
    openDefinition( path.toString() );
268
  }
269
270
  private void openDefinition( final String path ) {
271
    try {
272
      final DefinitionSource ds = createDefinitionSource( path );
273
      setDefinitionSource( ds );
274
      storeDefinitionSource();
275
276
      getDefinitionPane().setRoot( ds.asTreeView() );
277
    } catch( Exception e ) {
278
      alert( e );
279
    }
280
  }
281
282
  private void restoreDefinitionSource() {
283
    final Preferences preferences = getPreferences();
284
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
285
286
    if( source != null ) {
287
      openDefinition( source );
288
    }
289
  }
290
291
  private void storeDefinitionSource() {
292
    final Preferences preferences = getPreferences();
293
    final DefinitionSource ds = getDefinitionSource();
294
295
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
296
  }
297
298
  /**
299
   * Called when the last open tab is closed. This clears out the preview pane
300
   * and the definition pane.
301
   */
302
  private void closeRemainingTab() {
303
    getPreviewPane().clear();
304
    getDefinitionPane().clear();
305
  }
306
307
  /**
308
   * Called when an exception occurs that warrants the user's attention.
309
   *
310
   * @param e The exception with a message that the user should know about.
311
   */
312
  private void alert( final Exception e ) {
313
    // TODO: Update the status bar.
314
  }
315
316
  //---- File actions -------------------------------------------------------
317
  /**
318
   * Called when a file has been modified.
319
   *
320
   * @param snitch The watchdog file monitoring instance.
321
   * @param file The file that was modified.
322
   */
323
  @Override
324
  public void update( final Observable snitch, final Object file ) {
325
    if( file instanceof Path ) {
326
      update( (Path)file );
327
    }
328
  }
329
330
  /**
331
   * Called when a file has been modified.
332
   *
333
   * @param file Path to the modified file.
334
   */
335
  private void update( final Path file ) {
336
    // Avoid throwing IllegalStateException by running from a non-JavaFX thread.
337
    Platform.runLater(
338
      () -> {
339
        // Brute-force XSLT file reload by re-instantiating all processors.
340
        getProcessors().clear();
341
        refreshSelectedTab( getActiveFileEditor() );
342
      }
343
    );
344
  }
345
346
  //---- File actions -------------------------------------------------------
347
  private void fileNew() {
348
    getFileEditorPane().newEditor();
349
  }
350
351
  private void fileOpen() {
352
    getFileEditorPane().openFileDialog();
353
  }
354
355
  private void fileClose() {
356
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
357
  }
358
359
  private void fileCloseAll() {
360
    getFileEditorPane().closeAllEditors();
361
  }
362
363
  private void fileSave() {
364
    getFileEditorPane().saveEditor( getActiveFileEditor() );
365
  }
366
367
  private void fileSaveAll() {
368
    getFileEditorPane().saveAllEditors();
369
  }
370
371
  private void fileExit() {
372
    final Window window = getWindow();
373
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
374
  }
375
376
  //---- Help actions -------------------------------------------------------
377
  private void helpAbout() {
378
    Alert alert = new Alert( AlertType.INFORMATION );
379
    alert.setTitle( get( "Dialog.about.title" ) );
380
    alert.setHeaderText( get( "Dialog.about.header" ) );
381
    alert.setContentText( get( "Dialog.about.content" ) );
382
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
383
    alert.initOwner( getWindow() );
384
385
    alert.showAndWait();
386
  }
387
388
  //---- Convenience accessors ----------------------------------------------
389
  private float getFloat( final String key, final float defaultValue ) {
390
    return getPreferences().getFloat( key, defaultValue );
391
  }
392
393
  private Preferences getPreferences() {
394
    return getOptions().getState();
395
  }
396
397
  private Window getWindow() {
398
    return getScene().getWindow();
399
  }
400
401
  private MarkdownEditorPane getActiveEditor() {
402
    final EditorPane pane = getActiveFileEditor().getEditorPane();
403
404
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
405
  }
406
407
  private FileEditorTab getActiveFileEditor() {
408
    return getFileEditorPane().getActiveFileEditor();
409
  }
410
411
  //---- Member accessors ---------------------------------------------------
412
  public Scene getScene() {
413
    return this.scene;
414
  }
415
416
  private void setScene( Scene scene ) {
417
    this.scene = scene;
418
  }
419
420
  private Map<FileEditorTab, Processor<String>> getProcessors() {
421
    if( this.processors == null ) {
422
      this.processors = new HashMap<>();
423
    }
424
425
    return this.processors;
426
  }
427
  
428
  private ProcessorFactory getProcessorFactory() {
429
    if( this.processorFactory == null ) {
430
      this.processorFactory = createProcessorFactory();
431
    }
432
433
    return this.processorFactory;
434
  }
435
436
  private FileEditorTabPane getFileEditorPane() {
437
    if( this.fileEditorPane == null ) {
438
      this.fileEditorPane = createFileEditorPane();
439
    }
440
441
    return this.fileEditorPane;
442
  }
443
444
  private HTMLPreviewPane getPreviewPane() {
445
    if( this.previewPane == null ) {
446
      this.previewPane = createPreviewPane();
447
    }
448
449
    return this.previewPane;
450
  }
451
452
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
453
    this.definitionSource = definitionSource;
454
  }
455
456
  private DefinitionSource getDefinitionSource() {
457
    if( this.definitionSource == null ) {
458
      this.definitionSource = new EmptyDefinitionSource();
459
    }
460
461
    return this.definitionSource;
462
  }
463
464
  private DefinitionPane getDefinitionPane() {
465
    if( this.definitionPane == null ) {
466
      this.definitionPane = createDefinitionPane();
467
    }
468
469
    return this.definitionPane;
470
  }
471
472
  private Options getOptions() {
473
    return this.options;
474
  }
475
476
  private Snitch getSnitch() {
477
    return this.snitch;
478
  }
479
480
  public MenuBar getMenuBar() {
481
    return this.menuBar;
482
  }
483
484
  public void setMenuBar( MenuBar menuBar ) {
485
    this.menuBar = menuBar;
486
  }
487
488
  //---- Member creators ----------------------------------------------------
489
  /**
490
   * Factory to create processors that are suited to different file types.
491
   *
492
   * @param tab The tab that is subjected to processing.
493
   *
494
   * @return A processor suited to the file type specified by the tab's path.
495
   */
496
  private Processor<String> createProcessor( final FileEditorTab tab ) {
497
    return getProcessorFactory().createProcessor( tab );
498
  }
499
500
  private ProcessorFactory createProcessorFactory() {
501
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
502
  }
503
450504
  private DefinitionSource createDefinitionSource( final String path )
451505
    throws MalformedURLException {
M src/main/java/com/scrivenvar/Services.java
4040
public class Services {
4141
42
  private static final Map<Class, Object> SINGLETONS = new HashMap<>( 8 );
42
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
4343
4444
  /**
...
6060
  private static <T> T newInstance( final Class<T> api ) {
6161
    final ServiceLoader<T> services = ServiceLoader.load( api );
62
63
    T result = null;
6462
6563
    for( final T service : services ) {
66
      result = service;
67
68
      if( result != null ) {
69
        break;
64
      if( service != null ) {
65
        // Re-use the same instance the next time the class is loaded.
66
        put( api, service );
67
        return service;
7068
      }
71
    }
72
73
    if( result == null ) {
74
      throw new RuntimeException( "No implementation for: " + api );
7569
    }
76
77
    // Re-use the same instance the next time the class is loaded.
78
    put( api, result );
7970
80
    return result;
71
    throw new RuntimeException( "No implementation for: " + api );
8172
  }
8273
M src/main/java/com/scrivenvar/definition/DefinitionFactory.java
2828
package com.scrivenvar.definition;
2929
30
import com.scrivenvar.Services;
30
import com.scrivenvar.AbstractFileFactory;
31
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_FILE;
32
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_UNKNOWN;
33
import static com.scrivenvar.Constants.GLOB_PREFIX_DEFINITION;
34
import com.scrivenvar.FileType;
35
import static com.scrivenvar.FileType.YAML;
3136
import com.scrivenvar.definition.yaml.YamlFileDefinitionSource;
32
import com.scrivenvar.predicates.files.FileTypePredicate;
33
import com.scrivenvar.service.Settings;
3437
import java.io.File;
3538
import java.net.MalformedURLException;
3639
import java.net.URI;
3740
import java.net.URISyntaxException;
3841
import java.net.URL;
3942
import java.nio.file.Path;
4043
import java.nio.file.Paths;
41
import java.util.Iterator;
42
import java.util.List;
4344
4445
/**
4546
 * Responsible for creating objects that can read and write definition data
4647
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
4748
 * database.
4849
 *
4950
 * @author White Magic Software, Ltd.
5051
 */
51
public class DefinitionFactory {
52
53
  /**
54
   * Refers to filename extension settings in the configuration file. Do not
55
   * terminate this key prefix with a period.
56
   */
57
  private static final String EXTENSIONS_PREFIX = "file.ext.definition";
58
59
  private final Settings settings = Services.load( Settings.class );
52
public class DefinitionFactory extends AbstractFileFactory {
6053
6154
  /**
6255
   * Default (empty) constructor.
6356
   */
6457
  public DefinitionFactory() {
6558
  }
6659
6760
  /**
68
   * Creates a definition source that can read and write files that match the
69
   * given file type (from the path).
70
   *
71
   * @param path Reference to a variable definition file.
72
   *
73
   * @return
61
   * 
62
   * @param path
63
   * @return 
7464
   */
75
  public DefinitionSource fileDefinitionSource( final Path path ) {
76
    final Settings properties = getSettings();
77
    final Iterator<String> keys = properties.getKeys( EXTENSIONS_PREFIX );
78
79
    DefinitionSource result = new EmptyDefinitionSource();
80
81
    while( keys.hasNext() ) {
82
      final String key = keys.next();
83
      final List<String> patterns = properties.getStringSettingList( key );
84
      final FileTypePredicate predicate = new FileTypePredicate( patterns );
85
86
      if( predicate.test( path.toFile() ) ) {
87
        final String filetype = key.replace( EXTENSIONS_PREFIX + ".", "" );
88
89
        result = createFileDefinitionSource( filetype, path );
90
      }
91
    }
92
93
    return result;
94
  }
95
9665
  public DefinitionSource createDefinitionSource( final String path ) {
97
9866
    final String protocol = getProtocol( path );
9967
    DefinitionSource result = null;
10068
10169
    switch( protocol ) {
102
      case "file":
103
        result = fileDefinitionSource( Paths.get( path ) );
70
      case DEFINITION_PROTOCOL_FILE:
71
        final Path file = Paths.get( path );
72
        final FileType filetype = lookup( file, GLOB_PREFIX_DEFINITION );
73
        result = createFileDefinitionSource( filetype, file );
10474
        break;
10575
10676
      default:
107
        unknownDefinitionSource( protocol, path );
77
        unknownFileType( protocol, path );
10878
        break;
10979
    }
...
12191
   */
12292
  private DefinitionSource createFileDefinitionSource(
123
    final String filetype, final Path path ) {
124
    
93
    final FileType filetype, final Path path ) {
94
12595
    DefinitionSource result = null;
12696
12797
    switch( filetype ) {
128
      case "yaml":
98
      case YAML:
12999
        result = new YamlFileDefinitionSource( path );
130100
        break;
131101
132102
      default:
133
        unknownDefinitionSource( filetype, path.toString() );
103
        unknownFileType( filetype.toString(), path.toString() );
134104
        break;
135105
    }
136106
137107
    return result;
138
  }
139
140
  /**
141
   * Throws IllegalArgumentException because the given path could not be
142
   * recognized.
143
   *
144
   * @param type The detected path type (protocol, file extension, etc.).
145
   * @param path The path to a source of definitions.
146
   */
147
  private void unknownDefinitionSource( final String type, final String path ) {
148
    throw new IllegalArgumentException(
149
      "Unknown type '" + type + "' for " + path + "."
150
    );
151
  }
152
153
  private Settings getSettings() {
154
    return this.settings;
155108
  }
156109
...
200153
      result = file.toURI().toURL().getProtocol();
201154
    } catch( Exception e ) {
202
      result = "unknown";
155
      result = DEFINITION_PROTOCOL_UNKNOWN;
203156
    }
204157
M src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
4848
import org.fxmisc.richtext.StyleClassedTextArea;
4949
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
50
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
51
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
52
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
53
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
54
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
55
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
56
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
57
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
58
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
59
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
60
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
61
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
62
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
63
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
64
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
5065
5166
/**
M src/main/java/com/scrivenvar/processors/CaretInsertionProcessor.java
2929
3030
import static com.scrivenvar.Constants.CARET_POSITION_MD;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.SimpleIntegerProperty;
33
import javafx.beans.value.ObservableValue;
3134
3235
/**
...
3942
public abstract class CaretInsertionProcessor extends AbstractProcessor<String> {
4043
41
  private final int caretPosition;
44
  private final IntegerProperty caretPosition = new SimpleIntegerProperty();
4245
4346
  public CaretInsertionProcessor(
44
    final Processor<String> processor, final int position ) {
47
    final Processor<String> processor,
48
    final ObservableValue<Integer> position ) {
4549
    super( processor );
46
    this.caretPosition = position;
50
    this.caretPosition.bind( position );
4751
  }
4852
...
7074
   */
7175
  protected int getCaretPosition() {
72
    return this.caretPosition;
76
    return this.caretPosition.getValue();
7377
  }
7478
}
M src/main/java/com/scrivenvar/processors/MarkdownCaretInsertionProcessor.java
3030
import static java.lang.Character.isLetter;
3131
import static java.lang.Math.min;
32
import javafx.beans.value.ObservableValue;
3233
3334
/**
3435
 * Responsible for inserting a caret position token into a markdown document.
3536
 *
3637
 * @author White Magic Software, Ltd.
3738
 */
38
public  class MarkdownCaretInsertionProcessor extends CaretInsertionProcessor {
39
public class MarkdownCaretInsertionProcessor extends CaretInsertionProcessor {
3940
4041
  /**
4142
   * Constructs a processor capable of inserting a caret marker into Markdown.
4243
   *
4344
   * @param processor The next processor in the chain.
4445
   * @param position The caret's current position in the text.
4546
   */
4647
  public MarkdownCaretInsertionProcessor(
47
    final Processor<String> processor, final int position ) {
48
    final Processor<String> processor,
49
    final ObservableValue<Integer> position ) {
4850
    super( processor, position );
4951
  }
A src/main/java/com/scrivenvar/processors/ProcessorFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.Constants;
32
import com.scrivenvar.FileEditorTab;
33
import com.scrivenvar.FileType;
34
import com.scrivenvar.preview.HTMLPreviewPane;
35
import java.nio.file.Path;
36
import java.util.Map;
37
38
/**
39
 * Responsible for creating processors capable of parsing, transforming,
40
 * interpolating, and rendering known file types.
41
 *
42
 * @author White Magic Software, Ltd.
43
 */
44
public class ProcessorFactory extends AbstractFileFactory {
45
46
  private final HTMLPreviewPane previewPane;
47
  private final Map<String, String> resolvedMap;
48
49
  private Processor<String> terminalProcessChain;
50
51
  /**
52
   * Constructs a factory with the ability to create processors that can perform
53
   * text and caret processing to generate a final preview.
54
   *
55
   * @param previewPane
56
   * @param resolvedMap
57
   */
58
  public ProcessorFactory(
59
    final HTMLPreviewPane previewPane,
60
    final Map<String, String> resolvedMap ) {
61
    this.previewPane = previewPane;
62
    this.resolvedMap = resolvedMap;
63
  }
64
65
  /**
66
   * Creates a processor suitable for parsing and rendering the file opened at
67
   * the given tab.
68
   *
69
   * @param tab The tab containing a text editor, path, and caret position.
70
   *
71
   * @return A processor that can render the given tab's text.
72
   */
73
  public Processor<String> createProcessor( final FileEditorTab tab ) {
74
    final Path path = tab.getPath();
75
    final FileType fileType = lookup( path, Constants.GLOB_PREFIX_FILE );
76
    Processor<String> processor = null;
77
78
    switch( fileType ) {
79
      case RMARKDOWN:
80
        processor = createRMarkdownProcessor( tab );
81
        break;
82
83
      case MARKDOWN:
84
        processor = createMarkdownProcessor( tab );
85
        break;
86
87
      case XML:
88
        processor = createXMLProcessor( tab );
89
        break;
90
91
      default:
92
        unknownExtension( path );
93
        break;
94
    }
95
96
    return processor;
97
  }
98
99
  /**
100
   * Returns a processor common to all processors: markdown, caret position
101
   * token replacer, and an HTML preview renderer.
102
   *
103
   * @return Processors at the end of the processing chain.
104
   */
105
  private Processor<String> getTerminalProcessChain() {
106
    if( this.terminalProcessChain == null ) {
107
      this.terminalProcessChain = createTerminalProcessChain();
108
    }
109
110
    return this.terminalProcessChain;
111
  }
112
113
  private Processor<String> createTerminalProcessChain() {
114
    final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
115
    final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
116
    final Processor<String> mpp = new MarkdownProcessor( mcrp );
117
118
    return mpp;
119
  }
120
121
  protected Processor<String> createMarkdownProcessor( final FileEditorTab tab ) {
122
    final Processor<String> bp = getTerminalProcessChain();
123
    final Processor<String> xcip = new MarkdownCaretInsertionProcessor( bp, tab.caretPositionProperty() );
124
    final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
125
126
    return vp;
127
  }
128
129
  protected Processor<String> createRMarkdownProcessor( final FileEditorTab tab ) {
130
    return createMarkdownProcessor( tab );
131
  }
132
133
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
134
    final Processor<String> bp = getTerminalProcessChain();
135
    final Processor<String> xmlp = new XMLProcessor( bp, tab.getPath() );
136
    final Processor<String> xcip = new XMLCaretInsertionProcessor( xmlp, tab.caretPositionProperty() );
137
    final Processor<String> vp = new VariableProcessor( xcip, getResolvedMap() );
138
139
    return vp;
140
  }
141
142
  private HTMLPreviewPane getPreviewPane() {
143
    return this.previewPane;
144
  }
145
146
  /**
147
   * Returns the variable map of interpolated definitions.
148
   *
149
   * @return A map to help dereference variables.
150
   */
151
  private Map<String, String> getResolvedMap() {
152
    return this.resolvedMap;
153
  }
154
}
1155
M src/main/java/com/scrivenvar/processors/XMLCaretInsertionProcessor.java
2828
package com.scrivenvar.processors;
2929
30
import com.scrivenvar.FileEditorTab;
3130
import com.ximpleware.VTDException;
3231
import com.ximpleware.VTDGen;
3332
import static com.ximpleware.VTDGen.TOKEN_CHARACTER_DATA;
3433
import com.ximpleware.VTDNav;
3534
import java.text.ParseException;
35
import javafx.beans.value.ObservableValue;
3636
3737
/**
3838
 * Inserts a caret position indicator into the document.
3939
 *
4040
 * @author White Magic Software, Ltd.
4141
 */
4242
public class XMLCaretInsertionProcessor extends CaretInsertionProcessor {
4343
44
  private FileEditorTab tab;
44
  private VTDGen parser;
4545
4646
  /**
4747
   * Constructs a processor capable of inserting a caret marker into XML.
4848
   *
4949
   * @param processor The next processor in the chain.
5050
   * @param position The caret's current position in the text, cannot be null.
5151
   */
5252
  public XMLCaretInsertionProcessor(
53
    final Processor<String> processor, final int position ) {
53
    final Processor<String> processor,
54
    final ObservableValue<Integer> position ) {
5455
    super( processor, position );
5556
  }
5657
5758
  /**
5859
   * Inserts a caret at a valid position within the XML document.
5960
   *
60
   * @param t The string into which caret position marker text is inserted.
61
   * @param text The string into which caret position marker text is inserted.
6162
   *
62
   * @return t with a caret position marker included, or t if no place to insert
63
   * could be found.
63
   * @return The text with a caret position marker included, or the original
64
   * text if no insertion point could be found.
6465
   */
6566
  @Override
66
  public String processLink( final String t ) {
67
  public String processLink( final String text ) {
6768
    final int caret = getCaretPosition();
6869
    int insertOffset = -1;
69
70
    if( t.length() > 0 ) {
7170
71
    if( text.length() > 0 ) {
7272
      try {
73
        final VTDNav vn = getNavigator( t );
73
        final VTDNav vn = getNavigator( text );
7474
        final int tokens = vn.getTokenCount();
7575
...
8989
              final int prevLength = vn.getTokenLength( prevTokenIndex );
9090
91
              // If the caret falls within the limits of the previous token, then
92
              // insert the caret position marker at the caret offset.
91
              // If the caret falls within the limits of the previous token,
92
              // theninsert the caret position marker at the caret offset.
9393
              if( isBetween( caret, prevOffset, prevOffset + prevLength ) ) {
9494
                insertOffset = caret;
...
101101
                insertOffset = currOffset;
102102
              }
103
              
103
104104
              break;
105105
            }
...
118118
    }
119119
120
    return inject( t, insertOffset );
120
    return inject( text, insertOffset );
121121
  }
122122
...
142142
  }
143143
144
  private VTDGen getParser() {
144
  private synchronized VTDGen getParser() {
145
    if( this.parser == null ) {
146
      this.parser = createParser();
147
    }
148
149
    return this.parser;
150
  }
151
152
  /**
153
   * Creates a high-performance XML document parser.
154
   *
155
   * @return A new XML parser.
156
   */
157
  protected VTDGen createParser() {
145158
    return new VTDGen();
146159
  }
M src/main/java/com/scrivenvar/processors/XMLProcessor.java
3131
import com.scrivenvar.service.Snitch;
3232
import java.io.File;
33
import java.io.IOException;
3334
import java.io.Reader;
3435
import java.io.StringReader;
...
6364
 */
6465
public class XMLProcessor extends AbstractProcessor<String> {
65
  
66
6667
  private final Snitch snitch = Services.load( Snitch.class );
67
  
68
6869
  private XMLInputFactory xmlInputFactory;
6970
  private TransformerFactory transformerFactory;
70
  
71
  private Transformer transformer;
72
7173
  private Path path;
7274
...
114116
    final String template = getXsltFilename( text );
115117
    final Path xsl = getXslPath( template );
116
    
117
    // Listen for external file modification events.
118
    getWatchDog().listen( xsl );
119118
120119
    try(
121120
      final StringWriter output = new StringWriter( text.length() );
122121
      final StringReader input = new StringReader( text ) ) {
123
      
122
123
      // Listen for external file modification events.
124
      getSnitch().listen( xsl );
125
124126
      getTransformer( xsl ).transform(
125127
        new StreamSource( input ),
126128
        new StreamResult( output )
127129
      );
128
      
130
129131
      return output.toString();
130132
    }
131133
  }
132134
133135
  /**
134136
   * Returns an XSL transformer ready to transform an XML document using the
135137
   * XSLT file specified by the given path. If the path is already known then
136138
   * this will return the associated transformer.
137139
   *
138
   * @param path The path to an XSLT file.
140
   * @param xsl The path to an XSLT file.
139141
   *
140142
   * @return A transformer that will transform XML documents using the given
141143
   * XSLT file.
142144
   *
143145
   * @throws TransformerConfigurationException Could not instantiate the
144146
   * transformer.
145147
   */
146
  private Transformer getTransformer( final Path path )
148
  private Transformer getTransformer( final Path xsl )
149
    throws TransformerConfigurationException, IOException {
150
    if( this.transformer == null ) {
151
      this.transformer = createTransformer( xsl );
152
    }
153
154
    return this.transformer;
155
  }
156
157
  protected Transformer createTransformer( final Path xsl )
147158
    throws TransformerConfigurationException {
148
    
149
    final TransformerFactory factory = getTransformerFactory();
150
    final Source xslt = new StreamSource( path.toFile() );
151
    return factory.newTransformer( xslt );
159
    final Source xslt = new StreamSource( xsl.toFile() );
160
    return getTransformerFactory().newTransformer( xslt );
152161
  }
153
  
162
154163
  private Path getXslPath( final String filename ) {
155164
    final Path xmlPath = getPath();
156165
    final File xmlDirectory = xmlPath.toFile().getParentFile();
157
    
166
158167
    return Paths.get( xmlDirectory.getPath(), filename );
159168
  }
...
173182
  private String getXsltFilename( final String xml )
174183
    throws XMLStreamException, ParseException {
175
    
184
176185
    String result = "";
177
    
186
178187
    try( final StringReader sr = new StringReader( xml ) ) {
179188
      boolean found = false;
180189
      int count = 0;
181190
      final XMLEventReader reader = createXMLEventReader( sr );
182191
183192
      // If the processing instruction wasn't found in the first 10 lines,
184193
      // fail fast. This should iterate twice through the loop.
185194
      while( !found && reader.hasNext() && count++ < 10 ) {
186195
        final XMLEvent event = reader.nextEvent();
187
        
196
188197
        if( event.isProcessingInstruction() ) {
189198
          final ProcessingInstruction pi = (ProcessingInstruction)event;
190199
          final String target = pi.getTarget();
191
          
200
192201
          if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
193202
            result = getPseudoAttribute( pi.getData(), "href" );
194203
            found = true;
195204
          }
196205
        }
197206
      }
198
      
207
199208
      sr.close();
200209
    }
201
    
210
202211
    return result;
203212
  }
204
  
213
205214
  private XMLEventReader createXMLEventReader( final Reader reader )
206215
    throws XMLStreamException {
207216
    return getXMLInputFactory().createXMLEventReader( reader );
208217
  }
209
  
218
210219
  private synchronized XMLInputFactory getXMLInputFactory() {
211220
    if( this.xmlInputFactory == null ) {
212221
      this.xmlInputFactory = createXMLInputFactory();
213222
    }
214
    
223
215224
    return this.xmlInputFactory;
216225
  }
217
  
226
218227
  private XMLInputFactory createXMLInputFactory() {
219228
    return XMLInputFactory.newInstance();
220229
  }
221
  
230
222231
  private synchronized TransformerFactory getTransformerFactory() {
223232
    if( this.transformerFactory == null ) {
224233
      this.transformerFactory = createTransformerFactory();
225234
    }
226
    
235
227236
    return this.transformerFactory;
228237
  }
...
236245
    return new TransformerFactoryImpl();
237246
  }
238
  
247
239248
  private void setPath( final Path path ) {
240249
    this.path = path;
241250
  }
242
  
251
243252
  private Path getPath() {
244253
    return this.path;
245254
  }
246
  
247
  private Snitch getWatchDog() {
255
256
  private Snitch getSnitch() {
248257
    return this.snitch;
249258
  }
M src/main/java/com/scrivenvar/service/Snitch.java
3030
import java.io.IOException;
3131
import java.nio.file.Path;
32
import java.util.Observer;
3233
3334
/**
3435
 * Listens for changes to file system files and directories.
3536
 *
3637
 * @author White Magic Software, Ltd.
3738
 */
3839
public interface Snitch extends Service, Runnable {
40
41
  /**
42
   * Adds an observer to the set of observers for this object, provided that it
43
   * is not the same as some observer already in the set. The order in which
44
   * notifications will be delivered to multiple observers is not specified.
45
   *
46
   * @param o The object to receive changed events for when monitored files
47
   * are changed.
48
   */
49
  public void addObserver( Observer o );
3950
4051
  /**
M src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
2828
package com.scrivenvar.service.impl;
2929
30
import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT;
3031
import com.scrivenvar.service.Snitch;
3132
import java.io.IOException;
...
3940
import java.nio.file.WatchService;
4041
import java.util.Collections;
41
import java.util.HashMap;
42
import java.util.HashSet;
4342
import java.util.Map;
43
import java.util.Observable;
4444
import java.util.Set;
45
import java.util.concurrent.ConcurrentHashMap;
4546
4647
/**
47
 * Listens for file changes.
48
 * Listens for file changes. Other classes can register paths to be monitored
49
 * and listen for changes to those paths.
4850
 *
4951
 * @author White Magic Software, Ltd.
5052
 */
51
public class DefaultSnitch implements Snitch {
53
public class DefaultSnitch extends Observable implements Snitch {
5254
5355
  /**
...
147149
        // and timestamp updated. Instead, receive one ENTRY_MODIFY event
148150
        // with two counts.
149
        Thread.sleep( 50 );
151
        Thread.sleep( APP_WATCHDOG_TIMEOUT );
150152
151153
        for( final WatchEvent<?> event : key.pollEvents() ) {
152154
          final Path changed = path.resolve( (Path)event.context() );
153155
154156
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
155
            System.out.println( "RELOAD XSL: " + changed );
157
            setChanged();
158
            notifyObservers( changed );
156159
          }
157160
        }
...
167170
  }
168171
169
  private boolean isListening( final Path path ) {
170
    return getEavesdropped().contains( path );
172
  /**
173
   * Returns true if the list of files being listened to for changes contains
174
   * the given file.
175
   *
176
   * @param file Path to a system file.
177
   *
178
   * @return true The given file is being monitored for changes.
179
   */
180
  private boolean isListening( final Path file ) {
181
    return getEavesdropped().contains( file );
171182
  }
172183
...
191202
192203
  protected Map<WatchKey, Path> createWatchKeys() {
193
    return new HashMap<>();
204
    return new ConcurrentHashMap<>();
194205
  }
195206
...
208219
209220
  protected Set<Path> createEavesdropped() {
210
    return new HashSet<>();
221
    return ConcurrentHashMap.newKeySet();
211222
  }
212223
D src/main/resources/com/scrivenvar/editor/Markdown.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
.markdown-editor {
29
  -fx-font-size: 14px;
30
}
31
32
/*---- headers ----*/
33
34
.markdown-editor .h1 { -fx-font-size: 2.25em; }
35
.markdown-editor .h2 { -fx-font-size: 1.75em; }
36
.markdown-editor .h3 { -fx-font-size: 1.5em; }
37
.markdown-editor .h4 { -fx-font-size: 1.25em; }
38
.markdown-editor .h5 { -fx-font-size: 1.1em; }
39
.markdown-editor .h6 { -fx-font-size: 1em; }
40
41
.markdown-editor .h1,
42
.markdown-editor .h2,
43
.markdown-editor .h3,
44
.markdown-editor .h4,
45
.markdown-editor .h5,
46
.markdown-editor .h6 {
47
  -fx-font-weight: bold;
48
  -fx-fill: derive(crimson, -20%);
49
}
50
51
52
/*---- inlines ----*/
53
54
.markdown-editor .strong {
55
  -fx-font-weight: bold;
56
}
57
58
.markdown-editor .em {
59
  -fx-font-style: italic;
60
}
61
62
.markdown-editor .del {
63
  -fx-strikethrough: true;
64
}
65
66
.markdown-editor .a {
67
  -fx-fill: #4183C4 !important;
68
}
69
70
.markdown-editor .img {
71
  -fx-fill: #4183C4 !important;
72
}
73
74
.markdown-editor .code {
75
  -fx-font-family: monospace;
76
  -fx-fill: #090 !important;
77
}
78
79
80
/*---- blocks ----*/
81
82
.markdown-editor .pre {
83
  -fx-font-family: monospace;
84
  -fx-fill: #060 !important;
85
}
86
87
.markdown-editor .blockquote {
88
  -fx-fill: #777;
89
}
90
91
92
/*---- lists ----*/
93
94
.markdown-editor .ul {
95
}
96
97
.markdown-editor .ol {
98
}
99
100
.markdown-editor .li {
101
  -fx-fill: #444;
102
}
103
104
.markdown-editor .dl {
105
}
106
107
.markdown-editor .dt {
108
  -fx-font-weight: bold;
109
  -fx-font-style: italic;
110
}
111
112
.markdown-editor .dd {
113
  -fx-fill: #444;
114
}
115
116
117
/*---- table ----*/
118
119
.markdown-editor .table {
120
  -fx-font-family: monospace;
121
}
122
123
.markdown-editor .thead {
124
}
125
126
.markdown-editor .tbody {
127
}
128
129
.markdown-editor .caption {
130
}
131
132
.markdown-editor .th {
133
  -fx-font-weight: bold;
134
}
135
136
.markdown-editor .tr {
137
}
138
139
.markdown-editor .td {
140
}
141
142
143
/*---- misc ----*/
144
145
.markdown-editor .html {
146
  -fx-font-family: monospace;
147
  -fx-fill: derive(crimson, -50%);
148
}
149
.markdown-editor .monospace {
150
  -fx-font-family: monospace;
151
}
1521
A src/main/resources/com/scrivenvar/editor/markdown.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
.markdown-editor {
29
  -fx-font-size: 14px;
30
}
31
32
/*---- headers ----*/
33
34
.markdown-editor .h1 { -fx-font-size: 2.25em; }
35
.markdown-editor .h2 { -fx-font-size: 1.75em; }
36
.markdown-editor .h3 { -fx-font-size: 1.5em; }
37
.markdown-editor .h4 { -fx-font-size: 1.25em; }
38
.markdown-editor .h5 { -fx-font-size: 1.1em; }
39
.markdown-editor .h6 { -fx-font-size: 1em; }
40
41
.markdown-editor .h1,
42
.markdown-editor .h2,
43
.markdown-editor .h3,
44
.markdown-editor .h4,
45
.markdown-editor .h5,
46
.markdown-editor .h6 {
47
  -fx-font-weight: bold;
48
  -fx-fill: derive(crimson, -20%);
49
}
50
51
52
/*---- inlines ----*/
53
54
.markdown-editor .strong {
55
  -fx-font-weight: bold;
56
}
57
58
.markdown-editor .em {
59
  -fx-font-style: italic;
60
}
61
62
.markdown-editor .del {
63
  -fx-strikethrough: true;
64
}
65
66
.markdown-editor .a {
67
  -fx-fill: #4183C4 !important;
68
}
69
70
.markdown-editor .img {
71
  -fx-fill: #4183C4 !important;
72
}
73
74
.markdown-editor .code {
75
  -fx-font-family: monospace;
76
  -fx-fill: #090 !important;
77
}
78
79
80
/*---- blocks ----*/
81
82
.markdown-editor .pre {
83
  -fx-font-family: monospace;
84
  -fx-fill: #060 !important;
85
}
86
87
.markdown-editor .blockquote {
88
  -fx-fill: #777;
89
}
90
91
92
/*---- lists ----*/
93
94
.markdown-editor .ul {
95
}
96
97
.markdown-editor .ol {
98
}
99
100
.markdown-editor .li {
101
  -fx-fill: #444;
102
}
103
104
.markdown-editor .dl {
105
}
106
107
.markdown-editor .dt {
108
  -fx-font-weight: bold;
109
  -fx-font-style: italic;
110
}
111
112
.markdown-editor .dd {
113
  -fx-fill: #444;
114
}
115
116
117
/*---- table ----*/
118
119
.markdown-editor .table {
120
  -fx-font-family: monospace;
121
}
122
123
.markdown-editor .thead {
124
}
125
126
.markdown-editor .tbody {
127
}
128
129
.markdown-editor .caption {
130
}
131
132
.markdown-editor .th {
133
  -fx-font-weight: bold;
134
}
135
136
.markdown-editor .tr {
137
}
138
139
.markdown-editor .td {
140
}
141
142
143
/*---- misc ----*/
144
145
.markdown-editor .html {
146
  -fx-font-family: monospace;
147
  -fx-fill: derive(crimson, -50%);
148
}
149
.markdown-editor .monospace {
150
  -fx-font-family: monospace;
151
}
1152
M src/main/resources/com/scrivenvar/preview/webview.css
2727
=============================================================================*/
2828
29
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
29
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6,
30
p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn,
31
em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var,
32
b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label,
33
legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas,
34
details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output,
35
ruby, section, summary, time, mark, audio, video {
3036
  margin: 0;
3137
  padding: 0;
...
7278
}
7379
74
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
80
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
81
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
7582
  font-size: inherit;
7683
}
...
104111
}
105112
106
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child {
113
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2,
114
body>h3:first-child, body>h4:first-child, body>h5:first-child,
115
body>h6:first-child {
107116
  margin-top: 0;
108117
  padding-top: 0;
109118
}
110119
111
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
120
a:first-child h1, a:first-child h2, a:first-child h3,
121
a:first-child h4, a:first-child h5, a:first-child h6 {
112122
  margin-top: 0;
113123
  padding-top: 0;
...
308318
309319
#CARETPOSITION {
310
  border-right:1px solid #333;
320
  border-top: 2px solid #333;
321
  border-bottom: 2px solid #333;
322
  border-right: 1px solid #333;
311323
  margin-right:-1px;
312324
  animation: blink 1s linear infinite;
M src/main/resources/com/scrivenvar/settings.properties
99
application.messages= com.${application.title}.messages
1010
11
# Suppress multiple file modified notifications for one logical modification.
12
# Given in milliseconds.
13
application.watchdog.timeout=50
14
1115
# ########################################################################
1216
#
...
2731
2832
file.stylesheet.scene=${application.package}/scene.css
29
file.stylesheet.markdown=${application.package}/editor/Markdown.css
33
file.stylesheet.markdown=${application.package}/editor/markdown.css
3034
file.stylesheet.preview=webview.css
3135
...
4347
caret.token.base=CARETPOSITION
4448
caret.token.markdown=%${constant.caret.token.base}%
45
caret.token.xml=<![CDATA[${constant.caret.token.markdown}]]>
4649
caret.token.html=<span id="${caret.token.base}"></span>
4750
4851
# ########################################################################
4952
#
5053
# Filename Extensions
5154
#
5255
# ########################################################################
5356
5457
# Comma-separated list of definition filename extensions.
55
file.ext.definition.json=*.json
56
file.ext.definition.toml=*.toml
57
file.ext.definition.yaml=*.yml,*.yaml
58
file.ext.definition.properties=*.properties,*.props
58
definition.file.ext.json=*.json
59
definition.file.ext.toml=*.toml
60
definition.file.ext.yaml=*.yml,*.yaml
61
definition.file.ext.properties=*.properties,*.props
5962
6063
# Comma-separated list of filename extensions.
61
filter.file.ext.markdown=*.Rmd,*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt
62
filter.file.ext.definition=${file.ext.definition.yaml}
63
filter.file.ext.xml=*.xml,*.Rxml
64
filter.file.ext.all=*.*
64
file.ext.rmarkdown=*.Rmd
65
file.ext.rxml=*.Rxml
66
file.ext.markdown=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown}
67
file.ext.definition=${definition.file.ext.yaml}
68
file.ext.xml=*.xml,${file.ext.rxml}
69
file.ext.all=*.*
6570
6671
# ########################################################################