Dave Jarvis' Repositories

D CHANGES.md
1
# Change Log
2
3
## 0.9
4
5
- Associate variable name injector with all tabs.
6
7
## 0.8
8
9
- Load YAML variables from files
10
- Upgraded to Apache Commons Configuration 2.1
11
- Fixed bug with settings using comma-separated file extensions
12
13
## 0.7
14
15
- Added cursor to the preview pane
16
- Reconfigured constants to use settings
17
- Organized MainWindow code by similar method calls
18
- Added single entry point for refreshing file editor tab
19
20
## 0.6
21
22
- Revised synchronized scrolling with preview panel
23
- Added universal character encoding detection
24
- Removed options panel
25
- Decoupled Editor Tab and Preview Pane
26
27
## 0.5
28
29
- Added document processors for Markdown and Variables
30
- Simplified code base
31
- Added `Ctrl+Space` hot key for quick variable injection
32
- Replaced commonmark-java with flexmark
33
- Insert `CARETPOSITION` into document for preview pane scroll position reference
34
35
## 0.4
36
37
- Changed name to Scrivenvar
38
- Added hot-keys for variable mode and autocomplete
39
- Replaced pegdown with commonmark-java
40
- Started document processors to provide XSLT and variable dereferencing
41
42
## 0.3
43
44
- Changed name to Scrivendor
45
- Changed logo to match
46
- Started to implement service-oriented architecture
47
48
## 0.2
49
50
- RichTextFX (and dependencies) updated to version 0.6.10 (fixes bugs)
51
- pegdown Markdown parser updated to version 1.6
52
- Added five new pegdown 1.6 extension flags to Markdown Options tab
53
- Minor improvements
54
55
## 0.1
561
57
- Initial release
M build.gradle
1
version = '1.0.2'
1
version = '1.0.3'
22
33
apply plugin: 'java'
M src/main/java/com/scrivenvar/MainWindow.java
3030
import static com.scrivenvar.Constants.*;
3131
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
504
  private DefinitionSource createDefinitionSource( final String path )
505
    throws MalformedURLException {
506
    return createDefinitionFactory().createDefinitionSource( path );
507
  }
508
509
  /**
510
   * Create an editor pane to hold file editor tabs.
511
   *
512
   * @return A new instance, never null.
513
   */
514
  private FileEditorTabPane createFileEditorPane() {
515
    return new FileEditorTabPane();
516
  }
517
518
  private HTMLPreviewPane createPreviewPane() {
519
    return new HTMLPreviewPane();
520
  }
521
522
  private DefinitionPane createDefinitionPane() {
523
    return new DefinitionPane( getTreeView() );
524
  }
525
526
  private DefinitionFactory createDefinitionFactory() {
527
    return new DefinitionFactory();
528
  }
529
530
  private Node createMenuBar() {
531
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
532
533
    // File actions
534
    Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
535
    Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
536
    Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
537
    Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
538
    Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
539
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
540
    Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
541
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
542
    Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
543
544
    // Edit actions
545
    Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
546
      e -> getActiveEditor().undo(),
547
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
548
    Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
549
      e -> getActiveEditor().redo(),
550
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
551
552
    // Insert actions
553
    Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
554
      e -> getActiveEditor().surroundSelection( "**", "**" ),
555
      activeFileEditorIsNull );
556
    Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
557
      e -> getActiveEditor().surroundSelection( "*", "*" ),
558
      activeFileEditorIsNull );
559
    Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
560
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
561
      activeFileEditorIsNull );
562
    Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
563
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
564
      activeFileEditorIsNull );
565
    Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
566
      e -> getActiveEditor().surroundSelection( "`", "`" ),
567
      activeFileEditorIsNull );
568
    Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
569
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
570
      activeFileEditorIsNull );
571
572
    Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
573
      e -> getActiveEditor().insertLink(),
574
      activeFileEditorIsNull );
575
    Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
576
      e -> getActiveEditor().insertImage(),
577
      activeFileEditorIsNull );
578
579
    final Action[] headers = new Action[ 6 ];
580
581
    // Insert header actions (H1 ... H6)
582
    for( int i = 1; i <= 6; i++ ) {
583
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
584
      final String markup = String.format( "%n%n%s ", hashes );
585
      final String text = get( "Main.menu.insert.header_" + i );
586
      final String accelerator = "Shortcut+" + i;
587
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
588
589
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
590
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
591
        activeFileEditorIsNull );
592
    }
593
594
    Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
595
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
596
      activeFileEditorIsNull );
597
    Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
598
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
599
      activeFileEditorIsNull );
600
    Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
601
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
602
      activeFileEditorIsNull );
603
604
    // Help actions
605
    Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
606
607
    //---- MenuBar ----
608
    Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
609
      fileNewAction,
610
      fileOpenAction,
611
      null,
612
      fileCloseAction,
613
      fileCloseAllAction,
614
      null,
615
      fileSaveAction,
616
      fileSaveAllAction,
617
      null,
618
      fileExitAction );
619
620
    Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
621
      editUndoAction,
622
      editRedoAction );
623
624
    Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
625
      insertBoldAction,
626
      insertItalicAction,
627
      insertStrikethroughAction,
628
      insertBlockquoteAction,
629
      insertCodeAction,
630
      insertFencedCodeBlockAction,
631
      null,
632
      insertLinkAction,
633
      insertImageAction,
634
      null,
635
      headers[ 0 ],
636
      headers[ 1 ],
637
      headers[ 2 ],
638
      headers[ 3 ],
639
      headers[ 4 ],
640
      headers[ 5 ],
641
      null,
642
      insertUnorderedListAction,
643
      insertOrderedListAction,
644
      insertHorizontalRuleAction );
645
646
    Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
647
      helpAboutAction );
648
649
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
650
651
    //---- ToolBar ----
652
    ToolBar toolBar = ActionUtils.createToolBar(
653
      fileNewAction,
654
      fileOpenAction,
655
      fileSaveAction,
656
      null,
657
      editUndoAction,
658
      editRedoAction,
659
      null,
660
      insertBoldAction,
661
      insertItalicAction,
662
      insertBlockquoteAction,
663
      insertCodeAction,
664
      insertFencedCodeBlockAction,
665
      null,
666
      insertLinkAction,
667
      insertImageAction,
668
      null,
669
      headers[ 0 ],
670
      null,
671
      insertUnorderedListAction,
672
      insertOrderedListAction );
673
674
    return new VBox( menuBar, toolBar );
675
  }
676
677
  /**
678
   * Creates a boolean property that is bound to another boolean value of the
679
   * active editor.
680
   */
681
  private BooleanProperty createActiveBooleanProperty(
682
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
683
684
    final BooleanProperty b = new SimpleBooleanProperty();
685
    final FileEditorTab tab = getActiveFileEditor();
686
687
    if( tab != null ) {
688
      b.bind( func.apply( tab ) );
689
    }
690
691
    getFileEditorPane().activeFileEditorProperty().addListener(
692
      (observable, oldFileEditor, newFileEditor) -> {
693
        b.unbind();
694
695
        if( newFileEditor != null ) {
696
          b.bind( func.apply( newFileEditor ) );
697
        } else {
698
          b.set( false );
699
        }
700
      }
701
    );
702
703
    return b;
704
  }
705
706
  private void initLayout() {
707
    final SplitPane splitPane = new SplitPane(
708
      getDefinitionPane().getNode(),
709
      getFileEditorPane().getNode(),
710
      getPreviewPane().getNode() );
711
712
    splitPane.setDividerPositions(
713
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
714
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
715
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
716
717
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
718
    final BorderPane borderPane = new BorderPane();
719
    borderPane.setPrefSize( 1024, 800 );
720
    borderPane.setTop( createMenuBar() );
721
    borderPane.setCenter( splitPane );
722
32
import com.scrivenvar.definition.*;
33
import com.scrivenvar.editors.EditorPane;
34
import com.scrivenvar.editors.VariableNameInjector;
35
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
36
import com.scrivenvar.preview.HTMLPreviewPane;
37
import com.scrivenvar.processors.Processor;
38
import com.scrivenvar.processors.ProcessorFactory;
39
import com.scrivenvar.service.Options;
40
import com.scrivenvar.service.Snitch;
41
import com.scrivenvar.util.Action;
42
import com.scrivenvar.util.ActionUtils;
43
import static com.scrivenvar.util.StageState.*;
44
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
45
import java.nio.file.Path;
46
import java.util.HashMap;
47
import java.util.Map;
48
import java.util.Observable;
49
import java.util.Observer;
50
import java.util.function.Function;
51
import java.util.prefs.Preferences;
52
import javafx.application.Platform;
53
import javafx.beans.binding.Bindings;
54
import javafx.beans.binding.BooleanBinding;
55
import javafx.beans.property.BooleanProperty;
56
import javafx.beans.property.SimpleBooleanProperty;
57
import javafx.beans.value.ObservableBooleanValue;
58
import javafx.beans.value.ObservableValue;
59
import javafx.collections.ListChangeListener.Change;
60
import javafx.collections.ObservableList;
61
import static javafx.event.Event.fireEvent;
62
import javafx.scene.Node;
63
import javafx.scene.Scene;
64
import javafx.scene.control.Alert;
65
import javafx.scene.control.Alert.AlertType;
66
import javafx.scene.control.Menu;
67
import javafx.scene.control.MenuBar;
68
import javafx.scene.control.SplitPane;
69
import javafx.scene.control.Tab;
70
import javafx.scene.control.ToolBar;
71
import javafx.scene.control.TreeView;
72
import javafx.scene.image.Image;
73
import javafx.scene.image.ImageView;
74
import static javafx.scene.input.KeyCode.ESCAPE;
75
import javafx.scene.input.KeyEvent;
76
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
77
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
78
import javafx.scene.layout.BorderPane;
79
import javafx.scene.layout.VBox;
80
import javafx.stage.Window;
81
import javafx.stage.WindowEvent;
82
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
83
84
/**
85
 * Main window containing a tab pane in the center for file editors.
86
 *
87
 * @author Karl Tauber and White Magic Software, Ltd.
88
 */
89
public class MainWindow implements Observer {
90
  
91
  private final Options options = Services.load( Options.class );
92
  private final Snitch snitch = Services.load( Snitch.class );
93
  
94
  private Scene scene;
95
  private MenuBar menuBar;
96
  
97
  private DefinitionSource definitionSource;
98
  private DefinitionPane definitionPane;
99
  private FileEditorTabPane fileEditorPane;
100
  private HTMLPreviewPane previewPane;
101
102
  /**
103
   * Prevent re-instantiation processing classes.
104
   */
105
  private Map<FileEditorTab, Processor<String>> processors;
106
107
  public MainWindow() {
108
    initLayout();
109
    initOpenDefinitionListener();
110
    initTabAddedListener();
111
    initTabChangedListener();
112
    initPreferences();
113
    initWatchDog();
114
  }
115
116
  /**
117
   * Listen for file editor tab pane to receive an open definition source event.
118
   */
119
  private void initOpenDefinitionListener() {
120
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
121
      (ObservableValue<? extends Path> definitionFile,
122
        final Path oldPath, final Path newPath) -> {
123
        openDefinition( newPath );
124
        setProcessors( null );
125
        refreshSelectedTab( getActiveFileEditor() );
126
      }
127
    );
128
  }
129
130
  /**
131
   * When tabs are added, hook the various change listeners onto the new tab so
132
   * that the preview pane refreshes as necessary.
133
   */
134
  private void initTabAddedListener() {
135
    final FileEditorTabPane editorPane = getFileEditorPane();
136
137
    // Make sure the text processor kicks off when new files are opened.
138
    final ObservableList<Tab> tabs = editorPane.getTabs();
139
140
    // Update the preview pane on tab changes.
141
    tabs.addListener(
142
      (final Change<? extends Tab> change) -> {
143
        while( change.next() ) {
144
          if( change.wasAdded() ) {
145
            // Multiple tabs can be added simultaneously.
146
            for( final Tab newTab : change.getAddedSubList() ) {
147
              final FileEditorTab tab = (FileEditorTab)newTab;
148
              
149
              initTextChangeListener( tab );
150
              initCaretParagraphListener( tab );
151
              initVariableNameInjector( tab );
152
            }
153
          }
154
        }
155
      }
156
    );
157
  }
158
159
  /**
160
   * Reloads the preferences from the previous load.
161
   */
162
  private void initPreferences() {
163
    restoreDefinitionSource();
164
    getFileEditorPane().restorePreferences();
165
    updateDefinitionPane();
166
  }
167
168
  /**
169
   * Listen for new tab selection events.
170
   */
171
  private void initTabChangedListener() {
172
    final FileEditorTabPane editorPane = getFileEditorPane();
173
174
    // Update the preview pane changing tabs.
175
    editorPane.addTabSelectionListener(
176
      (ObservableValue<? extends Tab> tabPane,
177
        final Tab oldTab, final Tab newTab) -> {
178
179
        // If there was no old tab, then this is a first time load, which
180
        // can be ignored.
181
        if( oldTab != null ) {
182
          if( newTab == null ) {
183
            closeRemainingTab();
184
          } else {
185
            // Update the preview with the edited text.
186
            refreshSelectedTab( (FileEditorTab)newTab );
187
          }
188
        }
189
      }
190
    );
191
  }
192
  
193
  private void initTextChangeListener( final FileEditorTab tab ) {
194
    tab.addTextChangeListener(
195
      (ObservableValue<? extends String> editor,
196
        final String oldValue, final String newValue) -> {
197
        refreshSelectedTab( tab );
198
      }
199
    );
200
  }
201
  
202
  private void initCaretParagraphListener( final FileEditorTab tab ) {
203
    tab.addCaretParagraphListener(
204
      (ObservableValue<? extends Integer> editor,
205
        final Integer oldValue, final Integer newValue) -> {
206
        refreshSelectedTab( tab );
207
      }
208
    );
209
  }
210
  
211
  private void initVariableNameInjector( final FileEditorTab tab ) {
212
    VariableNameInjector.listen( tab, getDefinitionPane() );
213
  }
214
215
  /**
216
   * Watch for changes to external files. In particular, this awaits
217
   * modifications to any XSL files associated with XML files being edited. When
218
   * an XSL file is modified (external to the application), the watchdog's ears
219
   * perk up and the file is reloaded. This keeps the XSL transformation up to
220
   * date with what's on the file system.
221
   */
222
  private void initWatchDog() {
223
    getSnitch().addObserver( this );
224
  }
225
226
  /**
227
   * Called whenever the preview pane becomes out of sync with the file editor
228
   * tab. This can be called when the text changes, the caret paragraph changes,
229
   * or the file tab changes.
230
   *
231
   * @param tab The file editor tab that has been changed in some fashion.
232
   */
233
  private void refreshSelectedTab( final FileEditorTab tab ) {
234
    getPreviewPane().setPath( tab.getPath() );
235
    
236
    Processor<String> processor = getProcessors().get( tab );
237
    
238
    if( processor == null ) {
239
      processor = createProcessor( tab );
240
      getProcessors().put( tab, processor );
241
    }
242
    
243
    processor.processChain( tab.getEditorText() );
244
  }
245
246
  /**
247
   * Returns the variable map of interpolated definitions.
248
   *
249
   * @return A map to help dereference variables.
250
   */
251
  private Map<String, String> getResolvedMap() {
252
    return getDefinitionSource().getResolvedMap();
253
  }
254
255
  /**
256
   * Returns the root node for the hierarchical definition source.
257
   *
258
   * @return Data to display in the definition pane.
259
   */
260
  private TreeView<String> getTreeView() {
261
    try {
262
      return getDefinitionSource().asTreeView();
263
    } catch( Exception e ) {
264
      alert( e );
265
    }
266
    
267
    return new TreeView<>();
268
  }
269
270
  /**
271
   * Called when a definition file is opened.
272
   *
273
   * @param path Path to the file that was opened.
274
   */
275
  private void openDefinition( final Path path ) {
276
    openDefinition( path.toString() );
277
  }
278
279
  /**
280
   * Called to load a definition file from its source location.
281
   *
282
   * @param path The path to the definition file that was loaded.
283
   */
284
  private void openDefinition( final String path ) {
285
    try {
286
      final DefinitionSource ds = createDefinitionSource( path );
287
      setDefinitionSource( ds );
288
      storeDefinitionSource();
289
      updateDefinitionPane();
290
    } catch( Exception e ) {
291
      alert( e );
292
    }
293
  }
294
  
295
  private void updateDefinitionPane() {
296
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
297
  }
298
  
299
  private void restoreDefinitionSource() {
300
    final Preferences preferences = getPreferences();
301
    final String source = preferences.get( PREFS_DEFINITION_SOURCE, null );
302
    setDefinitionSource( createDefinitionSource( source ) );
303
  }
304
  
305
  private void storeDefinitionSource() {
306
    final Preferences preferences = getPreferences();
307
    final DefinitionSource ds = getDefinitionSource();
308
    
309
    preferences.put( PREFS_DEFINITION_SOURCE, ds.toString() );
310
  }
311
312
  /**
313
   * Called when the last open tab is closed. This clears out the preview pane
314
   * and the definition pane.
315
   */
316
  private void closeRemainingTab() {
317
    getPreviewPane().clear();
318
    getDefinitionPane().clear();
319
  }
320
321
  /**
322
   * Called when an exception occurs that warrants the user's attention.
323
   *
324
   * @param e The exception with a message that the user should know about.
325
   */
326
  private void alert( final Exception e ) {
327
    // TODO: Update the status bar.
328
  }
329
330
  //---- File actions -------------------------------------------------------
331
  /**
332
   * Called when a file has been modified.
333
   *
334
   * @param snitch The watchdog file monitoring instance.
335
   * @param file The file that was modified.
336
   */
337
  @Override
338
  public void update( final Observable snitch, final Object file ) {
339
    if( file instanceof Path ) {
340
      update( (Path)file );
341
    }
342
  }
343
344
  /**
345
   * Called when a file has been modified.
346
   *
347
   * @param file Path to the modified file.
348
   */
349
  private void update( final Path file ) {
350
    // Avoid throwing IllegalStateException by running from a non-JavaFX thread.
351
    Platform.runLater(
352
      () -> {
353
        // Brute-force XSLT file reload by re-instantiating all processors.
354
        resetProcessors();
355
        refreshSelectedTab( getActiveFileEditor() );
356
      }
357
    );
358
  }
359
360
  /**
361
   * After resetting the processors, they will refresh anew to be up-to-date
362
   * with the files (text and definition) currently loaded into the editor.
363
   */
364
  private void resetProcessors() {
365
    getProcessors().clear();
366
  }
367
368
  //---- File actions -------------------------------------------------------
369
  private void fileNew() {
370
    getFileEditorPane().newEditor();
371
  }
372
  
373
  private void fileOpen() {
374
    getFileEditorPane().openFileDialog();
375
  }
376
  
377
  private void fileClose() {
378
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
379
  }
380
  
381
  private void fileCloseAll() {
382
    getFileEditorPane().closeAllEditors();
383
  }
384
  
385
  private void fileSave() {
386
    getFileEditorPane().saveEditor( getActiveFileEditor() );
387
  }
388
  
389
  private void fileSaveAll() {
390
    getFileEditorPane().saveAllEditors();
391
  }
392
  
393
  private void fileExit() {
394
    final Window window = getWindow();
395
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
396
  }
397
398
  //---- Help actions -------------------------------------------------------
399
  private void helpAbout() {
400
    Alert alert = new Alert( AlertType.INFORMATION );
401
    alert.setTitle( get( "Dialog.about.title" ) );
402
    alert.setHeaderText( get( "Dialog.about.header" ) );
403
    alert.setContentText( get( "Dialog.about.content" ) );
404
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
405
    alert.initOwner( getWindow() );
406
    
407
    alert.showAndWait();
408
  }
409
410
  //---- Convenience accessors ----------------------------------------------
411
  private float getFloat( final String key, final float defaultValue ) {
412
    return getPreferences().getFloat( key, defaultValue );
413
  }
414
  
415
  private Preferences getPreferences() {
416
    return getOptions().getState();
417
  }
418
  
419
  private Window getWindow() {
420
    return getScene().getWindow();
421
  }
422
  
423
  private MarkdownEditorPane getActiveEditor() {
424
    final EditorPane pane = getActiveFileEditor().getEditorPane();
425
    
426
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
427
  }
428
  
429
  private FileEditorTab getActiveFileEditor() {
430
    return getFileEditorPane().getActiveFileEditor();
431
  }
432
433
  //---- Member accessors ---------------------------------------------------
434
  private void setScene( Scene scene ) {
435
    this.scene = scene;
436
  }
437
  
438
  public Scene getScene() {
439
    return this.scene;
440
  }
441
  
442
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
443
    this.processors = map;
444
  }
445
  
446
  private Map<FileEditorTab, Processor<String>> getProcessors() {
447
    if( this.processors == null ) {
448
      setProcessors( new HashMap<>() );
449
    }
450
    
451
    return this.processors;
452
  }
453
454
  private FileEditorTabPane getFileEditorPane() {
455
    if( this.fileEditorPane == null ) {
456
      this.fileEditorPane = createFileEditorPane();
457
    }
458
    
459
    return this.fileEditorPane;
460
  }
461
  
462
  private HTMLPreviewPane getPreviewPane() {
463
    if( this.previewPane == null ) {
464
      this.previewPane = createPreviewPane();
465
    }
466
    
467
    return this.previewPane;
468
  }
469
  
470
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
471
    this.definitionSource = definitionSource;
472
  }
473
  
474
  private DefinitionSource getDefinitionSource() {
475
    if( this.definitionSource == null ) {
476
      this.definitionSource = new EmptyDefinitionSource();
477
    }
478
    
479
    return this.definitionSource;
480
  }
481
  
482
  private DefinitionPane getDefinitionPane() {
483
    if( this.definitionPane == null ) {
484
      this.definitionPane = createDefinitionPane();
485
    }
486
    
487
    return this.definitionPane;
488
  }
489
  
490
  private Options getOptions() {
491
    return this.options;
492
  }
493
  
494
  private Snitch getSnitch() {
495
    return this.snitch;
496
  }
497
  
498
  public void setMenuBar( MenuBar menuBar ) {
499
    this.menuBar = menuBar;
500
  }
501
  
502
  public MenuBar getMenuBar() {
503
    return this.menuBar;
504
  }
505
506
  //---- Member creators ----------------------------------------------------
507
  /**
508
   * Factory to create processors that are suited to different file types.
509
   *
510
   * @param tab The tab that is subjected to processing.
511
   *
512
   * @return A processor suited to the file type specified by the tab's path.
513
   */
514
  private Processor<String> createProcessor( final FileEditorTab tab ) {
515
    return createProcessorFactory().createProcessor( tab );
516
  }
517
  
518
  private ProcessorFactory createProcessorFactory() {
519
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
520
  }
521
  
522
  private DefinitionSource createDefinitionSource( final String path ) {
523
    return createDefinitionFactory().createDefinitionSource( path );
524
  }
525
526
  /**
527
   * Create an editor pane to hold file editor tabs.
528
   *
529
   * @return A new instance, never null.
530
   */
531
  private FileEditorTabPane createFileEditorPane() {
532
    return new FileEditorTabPane();
533
  }
534
  
535
  private HTMLPreviewPane createPreviewPane() {
536
    return new HTMLPreviewPane();
537
  }
538
  
539
  private DefinitionPane createDefinitionPane() {
540
    return new DefinitionPane( getTreeView() );
541
  }
542
  
543
  private DefinitionFactory createDefinitionFactory() {
544
    return new DefinitionFactory();
545
  }
546
  
547
  private Node createMenuBar() {
548
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
549
550
    // File actions
551
    Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
552
    Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
553
    Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
554
    Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
555
    Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
556
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
557
    Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
558
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
559
    Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
560
561
    // Edit actions
562
    Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
563
      e -> getActiveEditor().undo(),
564
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
565
    Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
566
      e -> getActiveEditor().redo(),
567
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
568
569
    // Insert actions
570
    Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
571
      e -> getActiveEditor().surroundSelection( "**", "**" ),
572
      activeFileEditorIsNull );
573
    Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
574
      e -> getActiveEditor().surroundSelection( "*", "*" ),
575
      activeFileEditorIsNull );
576
    Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
577
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
578
      activeFileEditorIsNull );
579
    Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
580
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
581
      activeFileEditorIsNull );
582
    Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
583
      e -> getActiveEditor().surroundSelection( "`", "`" ),
584
      activeFileEditorIsNull );
585
    Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
586
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
587
      activeFileEditorIsNull );
588
    
589
    Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
590
      e -> getActiveEditor().insertLink(),
591
      activeFileEditorIsNull );
592
    Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
593
      e -> getActiveEditor().insertImage(),
594
      activeFileEditorIsNull );
595
    
596
    final Action[] headers = new Action[ 6 ];
597
598
    // Insert header actions (H1 ... H6)
599
    for( int i = 1; i <= 6; i++ ) {
600
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
601
      final String markup = String.format( "%n%n%s ", hashes );
602
      final String text = get( "Main.menu.insert.header_" + i );
603
      final String accelerator = "Shortcut+" + i;
604
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
605
      
606
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
607
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
608
        activeFileEditorIsNull );
609
    }
610
    
611
    Action insertUnorderedListAction = new Action( get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
612
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
613
      activeFileEditorIsNull );
614
    Action insertOrderedListAction = new Action( get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
615
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
616
      activeFileEditorIsNull );
617
    Action insertHorizontalRuleAction = new Action( get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
618
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
619
      activeFileEditorIsNull );
620
621
    // Help actions
622
    Action helpAboutAction = new Action( get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
623
624
    //---- MenuBar ----
625
    Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
626
      fileNewAction,
627
      fileOpenAction,
628
      null,
629
      fileCloseAction,
630
      fileCloseAllAction,
631
      null,
632
      fileSaveAction,
633
      fileSaveAllAction,
634
      null,
635
      fileExitAction );
636
    
637
    Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
638
      editUndoAction,
639
      editRedoAction );
640
    
641
    Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
642
      insertBoldAction,
643
      insertItalicAction,
644
      insertStrikethroughAction,
645
      insertBlockquoteAction,
646
      insertCodeAction,
647
      insertFencedCodeBlockAction,
648
      null,
649
      insertLinkAction,
650
      insertImageAction,
651
      null,
652
      headers[ 0 ],
653
      headers[ 1 ],
654
      headers[ 2 ],
655
      headers[ 3 ],
656
      headers[ 4 ],
657
      headers[ 5 ],
658
      null,
659
      insertUnorderedListAction,
660
      insertOrderedListAction,
661
      insertHorizontalRuleAction );
662
    
663
    Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
664
      helpAboutAction );
665
    
666
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, helpMenu );
667
668
    //---- ToolBar ----
669
    ToolBar toolBar = ActionUtils.createToolBar(
670
      fileNewAction,
671
      fileOpenAction,
672
      fileSaveAction,
673
      null,
674
      editUndoAction,
675
      editRedoAction,
676
      null,
677
      insertBoldAction,
678
      insertItalicAction,
679
      insertBlockquoteAction,
680
      insertCodeAction,
681
      insertFencedCodeBlockAction,
682
      null,
683
      insertLinkAction,
684
      insertImageAction,
685
      null,
686
      headers[ 0 ],
687
      null,
688
      insertUnorderedListAction,
689
      insertOrderedListAction );
690
    
691
    return new VBox( menuBar, toolBar );
692
  }
693
694
  /**
695
   * Creates a boolean property that is bound to another boolean value of the
696
   * active editor.
697
   */
698
  private BooleanProperty createActiveBooleanProperty(
699
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
700
    
701
    final BooleanProperty b = new SimpleBooleanProperty();
702
    final FileEditorTab tab = getActiveFileEditor();
703
    
704
    if( tab != null ) {
705
      b.bind( func.apply( tab ) );
706
    }
707
    
708
    getFileEditorPane().activeFileEditorProperty().addListener(
709
      (observable, oldFileEditor, newFileEditor) -> {
710
        b.unbind();
711
        
712
        if( newFileEditor != null ) {
713
          b.bind( func.apply( newFileEditor ) );
714
        } else {
715
          b.set( false );
716
        }
717
      }
718
    );
719
    
720
    return b;
721
  }
722
  
723
  private void initLayout() {
724
    final SplitPane splitPane = new SplitPane(
725
      getDefinitionPane().getNode(),
726
      getFileEditorPane().getNode(),
727
      getPreviewPane().getNode() );
728
    
729
    splitPane.setDividerPositions(
730
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
731
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
732
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
733
734
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
735
    final BorderPane borderPane = new BorderPane();
736
    borderPane.setPrefSize( 1024, 800 );
737
    borderPane.setTop( createMenuBar() );
738
    borderPane.setCenter( splitPane );
739
    
723740
    final Scene appScene = new Scene( borderPane );
724741
    setScene( appScene );
M src/main/java/com/scrivenvar/definition/DefinitionFactory.java
6060
  /**
6161
   * 
62
   * @param path
62
   * @param path Path to a file containing definitions.
6363
   * @return 
6464
   */
M src/main/java/com/scrivenvar/definition/DefinitionSource.java
2828
package com.scrivenvar.definition;
2929
30
import java.io.IOException;
3130
import java.util.Map;
3231
import javafx.scene.control.TreeView;
...
4544
   *
4645
   * @return A hierarchical tree suitable for displaying in the definition pane.
47
   *
48
   * @throws IOException Could not obtain the definition source data.
4946
   */
50
  public TreeView<String> asTreeView() throws IOException;
47
  public TreeView<String> asTreeView();
5148
5249
  /**
...
6259
   * Must return a re-loadable path to the data source. For a file, this is the
6360
   * absolute file path. For a database, this could be the JDBC connection. For
64
   * a web site, this might be the GET url.
61
   * a web site, this might be the GET URL.
6562
   *
6663
   * @return A non-null, non-empty string.
M src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.java
2828
package com.scrivenvar.definition;
2929
30
import java.io.IOException;
3130
import java.util.HashMap;
3231
import java.util.Map;
...
4443
4544
  @Override
46
  public TreeView<String> asTreeView() throws IOException {
45
  public TreeView<String> asTreeView() {
4746
    return new TreeView<>();
4847
  }
4948
5049
  @Override
5150
  public Map<String, String> getResolvedMap() {
5251
    return new HashMap<>();
5352
  }
54
5553
}
5654
M src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java
2929
3030
import static com.scrivenvar.Messages.get;
31
import java.io.IOException;
3231
import java.io.InputStream;
3332
import java.nio.file.Files;
...
4544
  private YamlTreeAdapter yamlTreeAdapter;
4645
  private YamlParser yamlParser;
46
  private TreeView<String> treeView;
4747
4848
  /**
4949
   * Constructs a new YAML definition source, populated from the given file.
5050
   *
5151
   * @param path Path to the YAML definition file.
5252
   */
5353
  public YamlFileDefinitionSource( final Path path ) {
5454
    super( path );
55
    init();
56
  }
57
  
58
  private void init() {
59
    setYamlParser( createYamlParser() );
5560
  }
5661
5762
  /**
5863
   * TODO: Associate variable file with path to current file.
5964
   *
6065
   * @return The TreeView for this definition source.
61
   *
62
   * @throws IOException
6366
   */
6467
  @Override
65
  public TreeView<String> asTreeView() throws IOException {
66
67
    try( final InputStream in = Files.newInputStream( getPath() ) ) {
68
      return getYamlTreeAdapter().adapt(
69
        in,
70
        get( "Pane.defintion.node.root.title" )
71
      );
68
  public TreeView<String> asTreeView() {
69
    if( this.treeView == null ) {
70
      this.treeView = createTreeView();
7271
    }
72
73
    return this.treeView;
7374
  }
7475
...
9293
  private YamlParser getYamlParser() {
9394
    if( this.yamlParser == null ) {
94
      setYamlParser( new YamlParser() );
95
      setYamlParser( createYamlParser() );
9596
    }
9697
9798
    return this.yamlParser;
9899
  }
99100
100101
  private void setYamlParser( final YamlParser yamlParser ) {
101102
    this.yamlParser = yamlParser;
103
  }
104
105
  private YamlParser createYamlParser() {
106
    try( final InputStream in = Files.newInputStream( getPath() ) ) {
107
      return new YamlParser( in );
108
    } catch( final Exception e ) {
109
      throw new RuntimeException( e );
110
    }
111
  }
112
113
  private TreeView<String> createTreeView() {
114
    return getYamlTreeAdapter().adapt(
115
      get( "Pane.defintion.node.root.title" )
116
    );
102117
  }
103118
}
M src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
7979
 * @author White Magic Software, Ltd.
8080
 */
81
public class YamlParser  {
82
83
  /**
84
   * Separates YAML variable nodes (e.g., the dots in
85
   * <code>$root.node.var$</code>).
86
   */
87
  public static final String SEPARATOR = ".";
88
  public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 );
89
90
  private final static int GROUP_DELIMITED = 1;
91
  private final static int GROUP_REFERENCE = 2;
92
93
  private final static VariableDecorator VARIABLE_DECORATOR
94
    = new YamlVariableDecorator();
95
96
  /**
97
   * Compiled version of DEFAULT_REGEX.
98
   */
99
  private final static Pattern REGEX_PATTERN
100
    = Pattern.compile( YamlVariableDecorator.REGEX );
101
102
  /**
103
   * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
104
   */
105
  private final static char SEPARATOR_YAML = '/';
106
107
  /**
108
   * Start of the Universe (the YAML document node that contains all others).
109
   */
110
  private ObjectNode documentRoot;
111
112
  /**
113
   * Map of references to dereferenced field values.
114
   */
115
  private Map<String, String> references;
116
117
  public YamlParser() {
118
  }
119
120
  /**
121
   * Returns the given string with all the delimited references swapped with
122
   * their recursively resolved values.
123
   *
124
   * @param text The text to parse with zero or more delimited references to
125
   * replace.
126
   *
127
   * @return The substituted value.
128
   */
129
  public String substitute( String text ) {
130
    final Matcher matcher = patternMatch( text );
131
    final Map<String, String> map = getReferences();
132
133
    while( matcher.find() ) {
134
      final String key = matcher.group( GROUP_DELIMITED );
135
      final String value = map.get( key );
136
137
      if( value == null ) {
138
        missing( text );
139
      } else {
140
        text = text.replace( key, value );
141
      }
142
    }
143
144
    return text;
145
  }
146
147
  /**
148
   * Returns all the strings with their values resolved in a flat hierarchy.
149
   * This copies all the keys and resolved values into a new map.
150
   *
151
   * @return The new map created with all values having been resolved,
152
   * recursively.
153
   */
154
  public Map<String, String> createResolvedMap() {
155
    final Map<String, String> map = new HashMap<>( 1024 );
156
157
    resolve( getDocumentRoot(), "", map );
158
159
    return map;
160
  }
161
162
  /**
163
   * Iterate over a given root node (at any level of the tree) and adapt each
164
   * leaf node.
165
   *
166
   * @param rootNode A JSON node (YAML node) to adapt.
167
   */
168
  private void resolve(
169
    final JsonNode rootNode, final String path, final Map<String, String> map ) {
170
171
    if( rootNode != null ) {
172
      rootNode.fields().forEachRemaining(
173
        (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map )
174
      );
175
    }
176
  }
177
178
  /**
179
   * Recursively adapt each rootNode to a corresponding rootItem.
180
   *
181
   * @param rootNode The node to adapt.
182
   */
183
  private void resolve(
184
    final Entry<String, JsonNode> rootNode,
185
    final String path,
186
    final Map<String, String> map ) {
187
188
    final JsonNode leafNode = rootNode.getValue();
189
    final String key = rootNode.getKey();
190
191
    if( leafNode.isValueNode() ) {
192
      final String value = rootNode.getValue().asText();
193
194
      map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) );
195
    }
196
197
    if( leafNode.isObject() ) {
198
      resolve( leafNode, path + key + SEPARATOR, map );
199
    }
200
  }
201
202
  /**
203
   * Reads the first document from the given stream of YAML data and returns a
204
   * corresponding object that represents the YAML hierarchy. The calling class
205
   * is responsible for closing the stream. Calling classes should use
206
   * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
207
   *
208
   * @param in The input stream containing YAML content.
209
   *
210
   * @return An object hierarchy to represent the content.
211
   *
212
   * @throws IOException Could not read the stream.
213
   */
214
  public JsonNode process( final InputStream in ) throws IOException {
215
216
    final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in );
217
    setDocumentRoot( root );
218
    process( root );
219
    return getDocumentRoot();
220
  }
221
222
  /**
223
   * Iterate over a given root node (at any level of the tree) and process each
224
   * leaf node.
225
   *
226
   * @param root A node to process.
227
   */
228
  private void process( final JsonNode root ) {
229
    root.fields().forEachRemaining( this::process );
230
  }
231
232
  /**
233
   * Process the given field, which is a named node. This is where the
234
   * application does the up-front work of mapping references to their fully
235
   * recursively dereferenced values.
236
   *
237
   * @param field The named node.
238
   */
239
  private void process( final Entry<String, JsonNode> field ) {
240
    final JsonNode node = field.getValue();
241
242
    if( node.isObject() ) {
243
      process( node );
244
    } else {
245
      final JsonNode fieldValue = field.getValue();
246
247
      // Only basic data types can be parsed into variable values. For
248
      // node structures, YAML has a built-in mechanism.
249
      if( fieldValue.isValueNode() ) {
250
        try {
251
          resolve( fieldValue.asText() );
252
        } catch( StackOverflowError e ) {
253
          throw new IllegalArgumentException(
254
            "Unresolvable: " + node.textValue() + " = " + fieldValue );
255
        }
256
      }
257
    }
258
  }
259
260
  /**
261
   * Inserts the delimited references and field values into the cache. This will
262
   * overwrite existing references.
263
   *
264
   * @param fieldValue YAML field containing zero or more delimited references.
265
   * If it contains a delimited reference, the parameter is modified with the
266
   * dereferenced value before it is returned.
267
   *
268
   * @return fieldValue without delimited references.
269
   */
270
  private String resolve( String fieldValue ) {
271
    final Matcher matcher = patternMatch( fieldValue );
272
273
    while( matcher.find() ) {
274
      final String delimited = matcher.group( GROUP_DELIMITED );
275
      final String reference = matcher.group( GROUP_REFERENCE );
276
      final String dereference = resolve( lookup( reference ) );
277
278
      fieldValue = fieldValue.replace( delimited, dereference );
279
280
      // This will perform some superfluous calls by overwriting existing
281
      // items in the delimited reference map.
282
      put( delimited, dereference );
283
    }
284
285
    return fieldValue;
286
  }
287
288
  /**
289
   * Inserts a key/value pair into the references map. The map retains
290
   * references and dereferenced values found in the YAML. If the reference
291
   * already exists, this will overwrite with a new value.
292
   *
293
   * @param delimited The variable name.
294
   * @param dereferenced The resolved value.
295
   */
296
  private void put( String delimited, String dereferenced ) {
297
    if( dereferenced.isEmpty() ) {
298
      missing( delimited );
299
    } else {
300
      getReferences().put( delimited, dereferenced );
301
    }
302
  }
303
304
  /**
305
   * Writes the modified YAML document to standard output.
306
   */
307
  private void writeDocument() throws IOException {
308
    getObjectMapper().writeValue( System.out, getDocumentRoot() );
309
  }
310
311
  /**
312
   * Called when a delimited reference is dereferenced to an empty string. This
313
   * should produce a warning for the user.
314
   *
315
   * @param delimited Delimited reference with no derived value.
316
   */
317
  private void missing( final String delimited ) {
318
    throw new InvalidParameterException(
319
      MessageFormat.format( "Missing value for '{0}'.", delimited ) );
320
  }
321
322
  /**
323
   * Returns a REGEX_PATTERN matcher for the given text.
324
   *
325
   * @param text The text that contains zero or more instances of a
326
   * REGEX_PATTERN that can be found using the regular expression.
327
   */
328
  private Matcher patternMatch( String text ) {
329
    return getPattern().matcher( text );
330
  }
331
332
  /**
333
   * Finds the YAML value for a reference.
334
   *
335
   * @param reference References a value in the YAML document.
336
   *
337
   * @return The dereferenced value.
338
   */
339
  private String lookup( final String reference ) {
340
    return getDocumentRoot().at( asPath( reference ) ).asText();
341
  }
342
343
  /**
344
   * Converts a reference (not delimited) to a path that can be used to find a
345
   * value that should exist inside the YAML document.
346
   *
347
   * @param reference The reference to convert to a YAML document path.
348
   *
349
   * @return The reference with a leading slash and its separator characters
350
   * converted to slashes.
351
   */
352
  private String asPath( final String reference ) {
353
    return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML );
354
  }
355
356
  /**
357
   * Sets the parent node for the entire YAML document tree.
358
   *
359
   * @param documentRoot The parent node.
360
   */
361
  private void setDocumentRoot( ObjectNode documentRoot ) {
362
    this.documentRoot = documentRoot;
363
  }
364
365
  /**
366
   * Returns the parent node for the entire YAML document tree.
367
   *
368
   * @return The parent node.
369
   */
370
  private ObjectNode getDocumentRoot() {
371
    return this.documentRoot;
372
  }
373
374
  /**
375
   * Returns the compiled regular expression REGEX_PATTERN used to match
376
   * delimited references.
377
   *
378
   * @return A compiled regex for use with the Matcher.
379
   */
380
  private Pattern getPattern() {
381
    return REGEX_PATTERN;
382
  }
383
384
  /**
385
   * Returns the list of references mapped to dereferenced values.
386
   *
387
   * @return
388
   */
389
  private Map<String, String> getReferences() {
390
    if( this.references == null ) {
391
      this.references = createReferences();
392
    }
393
394
    return this.references;
395
  }
396
397
  /**
398
   * Subclasses can override this method to insert their own map.
399
   *
400
   * @return An empty HashMap, never null.
401
   */
402
  protected Map<String, String> createReferences() {
403
    return new HashMap<>();
404
  }
405
406
  private final class ResolverYAMLFactory extends YAMLFactory {
407
408
    private static final long serialVersionUID = 1L;
409
410
    @Override
411
    protected YAMLGenerator _createGenerator(
412
      final Writer out, final IOContext ctxt ) throws IOException {
413
414
      return new ResolverYAMLGenerator(
415
        ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
416
        out, _version );
417
    }
418
  }
419
420
  private class ResolverYAMLGenerator extends YAMLGenerator {
421
422
    public ResolverYAMLGenerator(
423
      final IOContext ctxt,
424
      final int jsonFeatures,
425
      final int yamlFeatures,
426
      final ObjectCodec codec,
427
      final Writer out,
428
      final DumperOptions.Version version ) throws IOException {
429
430
      super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
431
    }
432
433
    @Override
434
    public void writeString( final String text )
435
      throws IOException, JsonGenerationException {
436
      super.writeString( substitute( text ) );
437
    }
438
  }
439
440
  private YAMLFactory getYAMLFactory() {
441
    return new ResolverYAMLFactory();
442
  }
443
81
public class YamlParser {
82
83
  /**
84
   * Separates YAML variable nodes (e.g., the dots in
85
   * <code>$root.node.var$</code>).
86
   */
87
  public static final String SEPARATOR = ".";
88
  public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 );
89
  
90
  private final static int GROUP_DELIMITED = 1;
91
  private final static int GROUP_REFERENCE = 2;
92
  
93
  private final static VariableDecorator VARIABLE_DECORATOR
94
    = new YamlVariableDecorator();
95
96
  /**
97
   * Compiled version of DEFAULT_REGEX.
98
   */
99
  private final static Pattern REGEX_PATTERN
100
    = Pattern.compile( YamlVariableDecorator.REGEX );
101
102
  /**
103
   * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
104
   */
105
  private final static char SEPARATOR_YAML = '/';
106
107
  /**
108
   * Start of the Universe (the YAML document node that contains all others).
109
   */
110
  private JsonNode documentRoot;
111
112
  /**
113
   * Map of references to dereferenced field values.
114
   */
115
  private Map<String, String> references;
116
  
117
  public YamlParser( final InputStream in ) throws IOException {
118
    process( in );
119
  }
120
121
  /**
122
   * Returns the given string with all the delimited references swapped with
123
   * their recursively resolved values.
124
   *
125
   * @param text The text to parse with zero or more delimited references to
126
   * replace.
127
   *
128
   * @return The substituted value.
129
   */
130
  public String substitute( String text ) {
131
    final Matcher matcher = patternMatch( text );
132
    final Map<String, String> map = getReferences();
133
    
134
    while( matcher.find() ) {
135
      final String key = matcher.group( GROUP_DELIMITED );
136
      final String value = map.get( key );
137
      
138
      if( value == null ) {
139
        missing( text );
140
      } else {
141
        text = text.replace( key, value );
142
      }
143
    }
144
    
145
    return text;
146
  }
147
148
  /**
149
   * Returns all the strings with their values resolved in a flat hierarchy.
150
   * This copies all the keys and resolved values into a new map.
151
   *
152
   * @return The new map created with all values having been resolved,
153
   * recursively.
154
   */
155
  public Map<String, String> createResolvedMap() {
156
    final Map<String, String> map = new HashMap<>( 1024 );
157
    
158
    resolve( getDocumentRoot(), "", map );
159
    
160
    return map;
161
  }
162
163
  /**
164
   * Iterate over a given root node (at any level of the tree) and adapt each
165
   * leaf node.
166
   *
167
   * @param rootNode A JSON node (YAML node) to adapt.
168
   * @param map Container that associates definitions with values.
169
   */
170
  private void resolve(
171
    final JsonNode rootNode, final String path, final Map<String, String> map ) {
172
    
173
    if( rootNode != null ) {
174
      rootNode.fields().forEachRemaining(
175
        (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map )
176
      );
177
    }
178
  }
179
180
  /**
181
   * Recursively adapt each rootNode to a corresponding rootItem.
182
   *
183
   * @param rootNode The node to adapt.
184
   */
185
  private void resolve(
186
    final Entry<String, JsonNode> rootNode,
187
    final String path,
188
    final Map<String, String> map ) {
189
    
190
    final JsonNode leafNode = rootNode.getValue();
191
    final String key = rootNode.getKey();
192
    
193
    if( leafNode.isValueNode() ) {
194
      final String value = rootNode.getValue().asText();
195
      
196
      map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) );
197
    }
198
    
199
    if( leafNode.isObject() ) {
200
      resolve( leafNode, path + key + SEPARATOR, map );
201
    }
202
  }
203
204
  /**
205
   * Reads the first document from the given stream of YAML data and returns a
206
   * corresponding object that represents the YAML hierarchy. The calling class
207
   * is responsible for closing the stream. Calling classes should use
208
   * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
209
   *
210
   * @param in The input stream containing YAML content.
211
   *
212
   * @return An object hierarchy to represent the content.
213
   *
214
   * @throws IOException Could not read the stream.
215
   */
216
  private JsonNode process( final InputStream in ) throws IOException {
217
    
218
    final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in );
219
    setDocumentRoot( root );
220
    process( root );
221
    return getDocumentRoot();
222
  }
223
224
  /**
225
   * Iterate over a given root node (at any level of the tree) and process each
226
   * leaf node.
227
   *
228
   * @param root A node to process.
229
   */
230
  private void process( final JsonNode root ) {
231
    root.fields().forEachRemaining( this::process );
232
  }
233
234
  /**
235
   * Process the given field, which is a named node. This is where the
236
   * application does the up-front work of mapping references to their fully
237
   * recursively dereferenced values.
238
   *
239
   * @param field The named node.
240
   */
241
  private void process( final Entry<String, JsonNode> field ) {
242
    final JsonNode node = field.getValue();
243
    
244
    if( node.isObject() ) {
245
      process( node );
246
    } else {
247
      final JsonNode fieldValue = field.getValue();
248
249
      // Only basic data types can be parsed into variable values. For
250
      // node structures, YAML has a built-in mechanism.
251
      if( fieldValue.isValueNode() ) {
252
        try {
253
          resolve( fieldValue.asText() );
254
        } catch( StackOverflowError e ) {
255
          throw new IllegalArgumentException(
256
            "Unresolvable: " + node.textValue() + " = " + fieldValue );
257
        }
258
      }
259
    }
260
  }
261
262
  /**
263
   * Inserts the delimited references and field values into the cache. This will
264
   * overwrite existing references.
265
   *
266
   * @param fieldValue YAML field containing zero or more delimited references.
267
   * If it contains a delimited reference, the parameter is modified with the
268
   * dereferenced value before it is returned.
269
   *
270
   * @return fieldValue without delimited references.
271
   */
272
  private String resolve( String fieldValue ) {
273
    final Matcher matcher = patternMatch( fieldValue );
274
    
275
    while( matcher.find() ) {
276
      final String delimited = matcher.group( GROUP_DELIMITED );
277
      final String reference = matcher.group( GROUP_REFERENCE );
278
      final String dereference = resolve( lookup( reference ) );
279
      
280
      fieldValue = fieldValue.replace( delimited, dereference );
281
282
      // This will perform some superfluous calls by overwriting existing
283
      // items in the delimited reference map.
284
      put( delimited, dereference );
285
    }
286
    
287
    return fieldValue;
288
  }
289
290
  /**
291
   * Inserts a key/value pair into the references map. The map retains
292
   * references and dereferenced values found in the YAML. If the reference
293
   * already exists, this will overwrite with a new value.
294
   *
295
   * @param delimited The variable name.
296
   * @param dereferenced The resolved value.
297
   */
298
  private void put( String delimited, String dereferenced ) {
299
    if( dereferenced.isEmpty() ) {
300
      missing( delimited );
301
    } else {
302
      getReferences().put( delimited, dereferenced );
303
    }
304
  }
305
306
  /**
307
   * Writes the modified YAML document to standard output.
308
   */
309
  private void writeDocument() throws IOException {
310
    getObjectMapper().writeValue( System.out, getDocumentRoot() );
311
  }
312
313
  /**
314
   * Called when a delimited reference is dereferenced to an empty string. This
315
   * should produce a warning for the user.
316
   *
317
   * @param delimited Delimited reference with no derived value.
318
   */
319
  private void missing( final String delimited ) {
320
    throw new InvalidParameterException(
321
      MessageFormat.format( "Missing value for '{0}'.", delimited ) );
322
  }
323
324
  /**
325
   * Returns a REGEX_PATTERN matcher for the given text.
326
   *
327
   * @param text The text that contains zero or more instances of a
328
   * REGEX_PATTERN that can be found using the regular expression.
329
   */
330
  private Matcher patternMatch( String text ) {
331
    return getPattern().matcher( text );
332
  }
333
334
  /**
335
   * Finds the YAML value for a reference.
336
   *
337
   * @param reference References a value in the YAML document.
338
   *
339
   * @return The dereferenced value.
340
   */
341
  private String lookup( final String reference ) {
342
    return getDocumentRoot().at( asPath( reference ) ).asText();
343
  }
344
345
  /**
346
   * Converts a reference (not delimited) to a path that can be used to find a
347
   * value that should exist inside the YAML document.
348
   *
349
   * @param reference The reference to convert to a YAML document path.
350
   *
351
   * @return The reference with a leading slash and its separator characters
352
   * converted to slashes.
353
   */
354
  private String asPath( final String reference ) {
355
    return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML );
356
  }
357
358
  /**
359
   * Sets the parent node for the entire YAML document tree.
360
   *
361
   * @param documentRoot The parent node.
362
   */
363
  private void setDocumentRoot( ObjectNode documentRoot ) {
364
    this.documentRoot = documentRoot;
365
  }
366
367
  /**
368
   * Returns the parent node for the entire YAML document tree.
369
   *
370
   * @return The parent node.
371
   */
372
  protected JsonNode getDocumentRoot() {
373
    return this.documentRoot;
374
  }
375
376
  /**
377
   * Returns the compiled regular expression REGEX_PATTERN used to match
378
   * delimited references.
379
   *
380
   * @return A compiled regex for use with the Matcher.
381
   */
382
  private Pattern getPattern() {
383
    return REGEX_PATTERN;
384
  }
385
386
  /**
387
   * Returns the list of references mapped to dereferenced values.
388
   *
389
   * @return
390
   */
391
  private Map<String, String> getReferences() {
392
    if( this.references == null ) {
393
      this.references = createReferences();
394
    }
395
    
396
    return this.references;
397
  }
398
399
  /**
400
   * Subclasses can override this method to insert their own map.
401
   *
402
   * @return An empty HashMap, never null.
403
   */
404
  protected Map<String, String> createReferences() {
405
    return new HashMap<>();
406
  }
407
  
408
  private final class ResolverYAMLFactory extends YAMLFactory {
409
    
410
    private static final long serialVersionUID = 1L;
411
    
412
    @Override
413
    protected YAMLGenerator _createGenerator(
414
      final Writer out, final IOContext ctxt ) throws IOException {
415
      
416
      return new ResolverYAMLGenerator(
417
        ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
418
        out, _version );
419
    }
420
  }
421
  
422
  private class ResolverYAMLGenerator extends YAMLGenerator {
423
    
424
    public ResolverYAMLGenerator(
425
      final IOContext ctxt,
426
      final int jsonFeatures,
427
      final int yamlFeatures,
428
      final ObjectCodec codec,
429
      final Writer out,
430
      final DumperOptions.Version version ) throws IOException {
431
      
432
      super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
433
    }
434
    
435
    @Override
436
    public void writeString( final String text )
437
      throws IOException, JsonGenerationException {
438
      super.writeString( substitute( text ) );
439
    }
440
  }
441
  
442
  private YAMLFactory getYAMLFactory() {
443
    return new ResolverYAMLFactory();
444
  }
445
  
444446
  private ObjectMapper getObjectMapper() {
445447
    return new ObjectMapper( getYAMLFactory() );
M src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
3030
import com.fasterxml.jackson.databind.JsonNode;
3131
import com.scrivenvar.definition.VariableTreeItem;
32
import java.io.IOException;
33
import java.io.InputStream;
3432
import java.util.Map.Entry;
3533
import javafx.scene.control.TreeItem;
...
5250
  /**
5351
   * Converts a YAML document to a TreeView based on the document keys. Only the
54
   * first document in the stream is adapted. This does not close the stream.
52
   * first document in the stream is adapted.
5553
   *
56
   * @param in Contains a YAML document.
5754
   * @param name Root TreeItem node name.
5855
   *
5956
   * @return A TreeView populated with all the keys in the YAML document.
60
   *
61
   * @throws IOException Could not read from the stream.
6257
   */
63
  public TreeView<String> adapt( final InputStream in, final String name )
64
    throws IOException {
65
66
    final JsonNode rootNode = getYamlParser().process( in );
58
  public TreeView<String> adapt( final String name ){
59
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
6760
    final TreeItem<String> rootItem = createTreeItem( name );
6861
D src/main/java/com/scrivenvar/test/TestDefinitionPane.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.test;
29
30
import com.scrivenvar.definition.DefinitionPane;
31
import static javafx.application.Application.launch;
32
import javafx.scene.control.TreeItem;
33
import javafx.scene.control.TreeView;
34
import javafx.stage.Stage;
35
36
/**
37
 * TestDefinitionPane application for debugging.
38
 */
39
public final class TestDefinitionPane extends TestHarness {
40
  /**
41
   * Application entry point.
42
   *
43
   * @param stage The primary application stage.
44
   *
45
   * @throws Exception Could not read configuration file.
46
   */
47
  @Override
48
  public void start( final Stage stage ) throws Exception {
49
    super.start( stage );
50
51
    TreeView<String> root = createTreeView();
52
    DefinitionPane pane = createDefinitionPane( root );
53
54
    test( pane, "language.ai.", "article" );
55
    test( pane, "language.ai", "ai" );
56
    test( pane, "l", "location" );
57
    test( pane, "la", "language" );
58
    test( pane, "c.p.n", "name" );
59
    test( pane, "c.p.n.", "First" );
60
    test( pane, "...", "c" );
61
    test( pane, "foo", "c" );
62
    test( pane, "foo.bar", "c" );
63
    test( pane, "", "c" );
64
    test( pane, "c", "protagonist" );
65
    test( pane, "c.", "protagonist" );
66
    test( pane, "c.p", "protagonist" );
67
    test( pane, "c.protagonist", "protagonist" );
68
69
    throw new RuntimeException( "Complete" );
70
  }
71
72
  private void test( DefinitionPane pane, String path, String value ) {
73
    System.out.println( "---------------------------" );
74
    System.out.println( "Find Path: '" + path + "'" );
75
    final TreeItem<String> node = pane.findNode( path );
76
    System.out.println( "Path Node: " + node );
77
    System.out.println( "Node Val : " + node.getValue() );
78
  }
79
80
  public static void main( String[] args ) {
81
    launch( args );
82
  }
83
}
841
D src/main/java/com/scrivenvar/test/TestHarness.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.test;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.definition.DefinitionPane;
32
import com.scrivenvar.definition.yaml.YamlParser;
33
import com.scrivenvar.definition.yaml.YamlTreeAdapter;
34
import java.io.IOException;
35
import java.io.InputStream;
36
import javafx.application.Application;
37
import javafx.scene.Scene;
38
import javafx.scene.control.TreeView;
39
import javafx.scene.layout.BorderPane;
40
import javafx.stage.Stage;
41
import org.fxmisc.flowless.VirtualizedScrollPane;
42
import org.fxmisc.richtext.StyleClassedTextArea;
43
44
/**
45
 * TestDefinitionPane application for debugging and head-banging.
46
 */
47
public abstract class TestHarness extends Application {
48
49
  private static Application app;
50
  private Scene scene;
51
52
  /**
53
   * Application entry point.
54
   *
55
   * @param stage The primary application stage.
56
   *
57
   * @throws Exception Could not read configuration file.
58
   */
59
  @Override
60
  public void start( final Stage stage ) throws Exception {
61
    initApplication();
62
    initScene();
63
    initStage( stage );
64
  }
65
  
66
  protected TreeView<String> createTreeView() throws IOException {
67
    return new YamlTreeAdapter( new YamlParser() ).adapt(
68
      asStream( "/com/scrivenvar/variables.yaml" ),
69
      get( "Pane.defintion.node.root.title" )
70
    );
71
  }
72
  
73
  protected DefinitionPane createDefinitionPane( TreeView<String> root ) {
74
    return new DefinitionPane( root );
75
  }
76
77
  private void initApplication() {
78
    app = this;
79
  }
80
81
  private void initScene() {
82
    final StyleClassedTextArea editor = new StyleClassedTextArea( false );
83
    final VirtualizedScrollPane<StyleClassedTextArea> scrollPane = new VirtualizedScrollPane<>( editor );
84
85
    final BorderPane borderPane = new BorderPane();
86
    borderPane.setPrefSize( 1024, 800 );
87
    borderPane.setCenter( scrollPane );
88
89
    setScene( new Scene( borderPane ) );
90
  }
91
92
  private void initStage( Stage stage ) {
93
    stage.setScene( getScene() );
94
  }
95
96
  private Scene getScene() {
97
    return this.scene;
98
  }
99
100
  private void setScene( Scene scene ) {
101
    this.scene = scene;
102
  }
103
104
  private static Application getApplication() {
105
    return app;
106
  }
107
108
  public static void showDocument( String uri ) {
109
    getApplication().getHostServices().showDocument( uri );
110
  }
111
112
  protected InputStream asStream( String resource ) {
113
    return TestHarness.class.getResourceAsStream( resource );
114
  }
115
}
1161
D src/main/java/com/scrivenvar/test/TestVariableNameProcessor.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.test;
29
30
import com.scrivenvar.definition.VariableTreeItem;
31
import java.util.Collection;
32
import java.util.HashMap;
33
import java.util.Map;
34
import static java.util.concurrent.ThreadLocalRandom.current;
35
import java.util.concurrent.TimeUnit;
36
import static java.util.concurrent.TimeUnit.DAYS;
37
import static java.util.concurrent.TimeUnit.HOURS;
38
import static java.util.concurrent.TimeUnit.MILLISECONDS;
39
import static java.util.concurrent.TimeUnit.MINUTES;
40
import static java.util.concurrent.TimeUnit.NANOSECONDS;
41
import static java.util.concurrent.TimeUnit.SECONDS;
42
import static javafx.application.Application.launch;
43
import javafx.scene.control.TreeItem;
44
import javafx.scene.control.TreeView;
45
import javafx.stage.Stage;
46
import org.ahocorasick.trie.*;
47
import org.ahocorasick.trie.Trie.TrieBuilder;
48
import static org.apache.commons.lang3.RandomStringUtils.randomNumeric;
49
import org.apache.commons.lang3.StringUtils;
50
51
/**
52
 * Tests substituting variable definitions with their values in a swath of text.
53
 *
54
 * @author White Magic Software, Ltd.
55
 */
56
public class TestVariableNameProcessor extends TestHarness {
57
58
  private final static int TEXT_SIZE = 1000000;
59
  private final static int MATCHES_DIVISOR = 1000;
60
61
  private final static StringBuilder SOURCE
62
    = new StringBuilder( randomNumeric( TEXT_SIZE ) );
63
64
  private final static boolean DEBUG = false;
65
66
  public TestVariableNameProcessor() {
67
  }
68
69
  @Override
70
  public void start( final Stage stage ) throws Exception {
71
    super.start( stage );
72
73
    final TreeView<String> treeView = createTreeView();
74
    final Map<String, String> definitions = new HashMap<>();
75
76
    populate( treeView.getRoot(), definitions );
77
    injectVariables( definitions );
78
79
    final String text = SOURCE.toString();
80
81
    show( text );
82
83
    long duration = System.nanoTime();
84
    String result = testBorAhoCorasick( text, definitions );
85
    duration = System.nanoTime() - duration;
86
    show( result );
87
    System.out.println( elapsed( duration ) );
88
89
    duration = System.nanoTime();
90
    result = testStringUtils( text, definitions );
91
    duration = System.nanoTime() - duration;
92
    show( result );
93
    System.out.println( elapsed( duration ) );
94
95
    throw new RuntimeException( "Complete" );
96
  }
97
98
  private void show( final String s ) {
99
    if( DEBUG ) {
100
      System.out.printf( "%s%n%n", s );
101
    }
102
  }
103
104
  private String testBorAhoCorasick(
105
    final String text,
106
    final Map<String, String> definitions ) {
107
    // Create a buffer sufficiently large that re-allocations are minimized.
108
    final StringBuilder sb = new StringBuilder( text.length() << 1 );
109
110
    final TrieBuilder builder = Trie.builder();
111
    builder.onlyWholeWords();
112
    builder.removeOverlaps();
113
114
    final String[] keys = keys( definitions );
115
116
    for( final String key : keys ) {
117
      builder.addKeyword( key );
118
    }
119
120
    final Trie trie = builder.build();
121
    final Collection<Emit> emits = trie.parseText( text );
122
123
    int prevIndex = 0;
124
125
    for( final Emit emit : emits ) {
126
      final int matchIndex = emit.getStart();
127
128
      sb.append( text.substring( prevIndex, matchIndex ) );
129
      sb.append( definitions.get( emit.getKeyword() ) );
130
      prevIndex = emit.getEnd() + 1;
131
    }
132
133
    // Add the remainder of the string (contains no more matches).
134
    sb.append( text.substring( prevIndex ) );
135
136
    return sb.toString();
137
  }
138
139
  private String testStringUtils(
140
    final String text, final Map<String, String> definitions ) {
141
    final String[] keys = keys( definitions );
142
    final String[] values = values( definitions );
143
144
    return StringUtils.replaceEach( text, keys, values );
145
  }
146
147
  private String[] keys( final Map<String, String> definitions ) {
148
    final int size = definitions.size();
149
    return definitions.keySet().toArray( new String[ size ] );
150
  }
151
152
  private String[] values( final Map<String, String> definitions ) {
153
    final int size = definitions.size();
154
    return definitions.values().toArray( new String[ size ] );
155
  }
156
157
  /**
158
   * Decomposes a period of time into days, hours, minutes, seconds,
159
   * milliseconds, and nanoseconds.
160
   *
161
   * @param duration Time in nanoseconds.
162
   *
163
   * @return A non-null, comma-separated string (without newline).
164
   */
165
  public String elapsed( long duration ) {
166
    final TimeUnit scale = NANOSECONDS;
167
168
    long days = scale.toDays( duration );
169
    duration -= DAYS.toMillis( days );
170
    long hours = scale.toHours( duration );
171
    duration -= HOURS.toMillis( hours );
172
    long minutes = scale.toMinutes( duration );
173
    duration -= MINUTES.toMillis( minutes );
174
    long seconds = scale.toSeconds( duration );
175
    duration -= SECONDS.toMillis( seconds );
176
    long millis = scale.toMillis( duration );
177
    duration -= MILLISECONDS.toMillis( seconds );
178
    long nanos = scale.toNanos( duration );
179
180
    return String.format(
181
      "%d days, %d hours, %d minutes, %d seconds, %d millis, %d nanos",
182
      days, hours, minutes, seconds, millis, nanos
183
    );
184
  }
185
186
  private void injectVariables( final Map<String, String> definitions ) {
187
    for( int i = (SOURCE.length() / MATCHES_DIVISOR) + 1; i > 0; i-- ) {
188
      final int r = current().nextInt( 1, SOURCE.length() );
189
      SOURCE.insert( r, randomKey( definitions ) );
190
    }
191
  }
192
193
  private String randomKey( final Map<String, String> map ) {
194
    final Object[] keys = map.keySet().toArray();
195
    final int r = current().nextInt( keys.length );
196
    return keys[ r ].toString();
197
  }
198
199
  private void populate( final TreeItem<String> parent, final Map<String, String> map ) {
200
    for( final TreeItem<String> child : parent.getChildren() ) {
201
      if( child.isLeaf() ) {
202
        final VariableTreeItem<String> item;
203
204
        if( child instanceof VariableTreeItem ) {
205
          item = ((VariableTreeItem<String>)child);
206
        } else {
207
          throw new IllegalArgumentException(
208
            "Child must be subclass of VariableTreeItem: " + child );
209
        }
210
211
        final String key = asDefinition( item.toPath() );
212
        final String value = child.getValue();
213
214
        map.put( key, value );
215
      } else {
216
        populate( child, map );
217
      }
218
    }
219
  }
220
221
  private String asDefinition( final String key ) {
222
    return "$" + key + "$";
223
  }
224
225
  public static void main( String[] args ) {
226
    launch( args );
227
  }
228
}
2291