Dave Jarvis' Repositories

M .gitattributes
1515
*.png binary
1616
*.zip binary
17
*.ttf binary
18
1719
M USAGE-R.md
55
interpreter known as [Renjin](https://www.renjin.org/) to integrate with R.
66
7
# Hello World
7
# Hello world
88
99
Complete the following steps to see R in action:
...
2929
```
3030
31
# Bootstrap Script
31
# Bootstrap script
3232
3333
Being able to run R code while editing an R Markdown document is convenient.
...
7171
a `sum` function that is called by name in the Markdown document.
7272
73
# Working Directory
73
# Working directory
7474
7575
R files may be sourced from any directory, not just the user's home
...
110110
working directory where the R engine searches for source files.
111111
112
# YAML Definitions
112
# YAML definitions
113113
114114
To see how variable definitions work in R, try the following:
A USAGE-SVG.md
1
# Introduction
2
3
The Scalable Vector Graphics (SVG) drawing software---[Batik](https://xmlgraphics.apache.org/batik/)---that's used by the application may be unable to read certain SVG files produced by [Inkscape](https://inkscape.org/). The result is that embedding the vector graphics files may trigger the following issues:
4
5
* Unable to create nested element
6
* Black blocks, no text displayed
7
* Black text instead of coloured
8
9
The remainder of this document explains these problems and how to fix them.
10
11
# Nested element
12
13
When referencing a vector graphic using Markdown, the status bar may show the following error:
14
15
> The current document is unable to create an element of the requested type (namespace: http://www.w3.org/2000/svg, name: flowRoot).
16
17
This error is due to a version mismatch of the `flowRoot` element that Inkscape creates.
18
19
## Fix
20
21
Resolve the issue by changing the SVG version number as follows:
22
23
1. Edit the vector graphics file using any text editor.
24
1. Find `version="1.1"` and change it to `version="1.2"`.
25
1. Save the file.
26
27
The SVG will now appear inside the application; however, the text may appear as black blocks.
28
29
# Black blocks
30
31
Depending on how text is added to a vector graphic in Inkscape, the text may be inserted within an element called a `flowRoot`. Although Batik recognizes `flowRoot` for SVG version 1.2, it cannot fully interpret the contents. Black blocks are drawn instead of the text, such as those depicted in the following figure:
32
33
![Missing text](images/blocked-text.png)
34
35
## Fix
36
37
Resolve the issue by "unflowing" all text elements as follows:
38
39
1. Start Inkscape.
40
1. Load the SVG file.
41
1. Select all the text elements.
42
1. Click **Text → Unflow**.
43
44
The text may change size and position; recreate the text without dragging using the text tool. After all the text areas have been recreated, continue as follows:
45
46
1. Click **Edit → XML Editor**.
47
1. Expand the **XML Editor** to see more elements.
48
1. Delete all elements named `svg:flowRoot`.
49
1. Save the file.
50
51
When the illustration is reloaded, the black blocks will have disappeared, but the text elements ignore any assigned colour.
52
53
# Black text
54
55
When an SVG `style` attribute contains a reference to `-inkscape-font-specification`, Batik ignores all values that follow said reference. This results in black text, such as:
56
57
![Black text](images/black-text.png)
58
59
## Fix
60
61
Resolve the issue of colourless text as follows:
62
63
1. Open the SVG file in a plain text editor.
64
1. Remove all references `-inkscape-font-specification:'<FONT>';`, including the trailing (or leading) semicolon.
65
1. Save the file.
66
67
When the illustration is reloaded, the colours will have reappeared, such as:
68
69
![Resolved text](images/resolved-text.png)
70
171
M USAGE.md
33
This document describes how to use the application.
44
5
# Variable Definitions
5
# Variable definitions
66
77
Variable definitions provide a way to insert key names having associated values into a document. The variable names and values are declared inside an external file using the [YAML](http://www.yaml.org/) file format. Simply put, variables are written in the file as follows:
...
4646
Save the variable definitions in a file having an extension of `.yaml` or `.yml`.
4747
48
# Document Editing
48
# Document editing
4949
5050
The application's purpose is to completely separate the document's content from its presentation. To achieve this, documents are composed using a [plain text](http://spec.commonmark.org/0.28/) format.
5151
52
## Create Document
52
## Create document
5353
5454
Start a new document as follows:
...
6363
The variable definitions appear in the variable definition pane under the heading of **Definitions**.
6464
65
## Edit Document
65
## Edit document
6666
6767
Edit the document as normal. Notice how the preview pane updates as new content is added. The toolbar shows various icons that perform different formatting operations. Try them to see how they appear in the preview pane. Other operations not shown on the toolbar include:
6868
6969
* Struck text (enclose the words within `~~` and `~~`)
7070
* Horizontal rule (use `---` on an otherwise empty line).
7171
7272
The preview pane shows one way to interpret and format the document, but many other presentations are possible.
7373
74
## Insert Variable
74
## Insert variable
7575
7676
Let's assume that the variable definitions loaded into the application include:
M build.gradle
2525
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.6.0'
2626
  implementation 'com.vladsch.flexmark:flexmark:0.62.2'
27
  implementation 'com.vladsch.flexmark:flexmark-ext-tables:0.62.2'
28
  implementation 'com.vladsch.flexmark:flexmark-ext-superscript:0.62.2'
27
  implementation 'com.vladsch.flexmark:flexmark-ext-definition:0.62.2'
2928
  implementation 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.62.2'
29
  implementation 'com.vladsch.flexmark:flexmark-ext-superscript:0.62.2'
30
  implementation 'com.vladsch.flexmark:flexmark-ext-tables:0.62.2'
31
  implementation 'com.vladsch.flexmark:flexmark-ext-typographic:0.62.2'
3032
  implementation 'com.fasterxml.jackson.core:jackson-core:2.11.0'
3133
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
A images/black-text.png
Binary file
A images/blocked-text.png
Binary file
M images/logo64.png
Binary file
A images/resolved-text.png
Binary file
A licenses/fonts/FIRACODE.txt
1
Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
2
3
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
This license is copied below, and is also available with a FAQ at:
5
http://scripts.sil.org/OFL
6
7
8
-----------------------------------------------------------
9
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
-----------------------------------------------------------
11
12
PREAMBLE
13
The goals of the Open Font License (OFL) are to stimulate worldwide
14
development of collaborative font projects, to support the font creation
15
efforts of academic and linguistic communities, and to provide a free and
16
open framework in which fonts may be shared and improved in partnership
17
with others.
18
19
The OFL allows the licensed fonts to be used, studied, modified and
20
redistributed freely as long as they are not sold by themselves. The
21
fonts, including any derivative works, can be bundled, embedded,
22
redistributed and/or sold with any software provided that any reserved
23
names are not used by derivative works. The fonts and derivatives,
24
however, cannot be released under any other type of license. The
25
requirement for fonts to remain under this license does not apply
26
to any document created using the fonts or their derivatives.
27
28
DEFINITIONS
29
"Font Software" refers to the set of files released by the Copyright
30
Holder(s) under this license and clearly marked as such. This may
31
include source files, build scripts and documentation.
32
33
"Reserved Font Name" refers to any names specified as such after the
34
copyright statement(s).
35
36
"Original Version" refers to the collection of Font Software components as
37
distributed by the Copyright Holder(s).
38
39
"Modified Version" refers to any derivative made by adding to, deleting,
40
or substituting -- in part or in whole -- any of the components of the
41
Original Version, by changing formats or by porting the Font Software to a
42
new environment.
43
44
"Author" refers to any designer, engineer, programmer, technical
45
writer or other person who contributed to the Font Software.
46
47
PERMISSION & CONDITIONS
48
Permission is hereby granted, free of charge, to any person obtaining
49
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
redistribute, and sell modified and unmodified copies of the Font
51
Software, subject to the following conditions:
52
53
1) Neither the Font Software nor any of its individual components,
54
in Original or Modified Versions, may be sold by itself.
55
56
2) Original or Modified Versions of the Font Software may be bundled,
57
redistributed and/or sold with any software, provided that each copy
58
contains the above copyright notice and this license. These can be
59
included either as stand-alone text files, human-readable headers or
60
in the appropriate machine-readable metadata fields within text or
61
binary files as long as those fields can be easily viewed by the user.
62
63
3) No Modified Version of the Font Software may use the Reserved Font
64
Name(s) unless explicit written permission is granted by the corresponding
65
Copyright Holder. This restriction only applies to the primary font name as
66
presented to the users.
67
68
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
Software shall not be used to promote, endorse or advertise any
70
Modified Version, except to acknowledge the contribution(s) of the
71
Copyright Holder(s) and the Author(s) or with their explicit written
72
permission.
73
74
5) The Font Software, modified or unmodified, in part or in whole,
75
must be distributed entirely under this license, and must not be
76
distributed under any other license. The requirement for fonts to
77
remain under this license does not apply to any document created
78
using the Font Software.
79
80
TERMINATION
81
This license becomes null and void if any of the above conditions are
82
not met.
83
84
DISCLAIMER
85
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
OTHER DEALINGS IN THE FONT SOFTWARE.
194
A licenses/fonts/VOLLKORN.txt
1
Copyright 2017 The Vollkorn Project Authors (https://github.com/FAlthausen/Vollkorn-Typeface)
2
3
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
This license is copied below, and is also available with a FAQ at:
5
http://scripts.sil.org/OFL
6
7
8
-----------------------------------------------------------
9
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
-----------------------------------------------------------
11
12
PREAMBLE
13
The goals of the Open Font License (OFL) are to stimulate worldwide
14
development of collaborative font projects, to support the font creation
15
efforts of academic and linguistic communities, and to provide a free and
16
open framework in which fonts may be shared and improved in partnership
17
with others.
18
19
The OFL allows the licensed fonts to be used, studied, modified and
20
redistributed freely as long as they are not sold by themselves. The
21
fonts, including any derivative works, can be bundled, embedded, 
22
redistributed and/or sold with any software provided that any reserved
23
names are not used by derivative works. The fonts and derivatives,
24
however, cannot be released under any other type of license. The
25
requirement for fonts to remain under this license does not apply
26
to any document created using the fonts or their derivatives.
27
28
DEFINITIONS
29
"Font Software" refers to the set of files released by the Copyright
30
Holder(s) under this license and clearly marked as such. This may
31
include source files, build scripts and documentation.
32
33
"Reserved Font Name" refers to any names specified as such after the
34
copyright statement(s).
35
36
"Original Version" refers to the collection of Font Software components as
37
distributed by the Copyright Holder(s).
38
39
"Modified Version" refers to any derivative made by adding to, deleting,
40
or substituting -- in part or in whole -- any of the components of the
41
Original Version, by changing formats or by porting the Font Software to a
42
new environment.
43
44
"Author" refers to any designer, engineer, programmer, technical
45
writer or other person who contributed to the Font Software.
46
47
PERMISSION & CONDITIONS
48
Permission is hereby granted, free of charge, to any person obtaining
49
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
redistribute, and sell modified and unmodified copies of the Font
51
Software, subject to the following conditions:
52
53
1) Neither the Font Software nor any of its individual components,
54
in Original or Modified Versions, may be sold by itself.
55
56
2) Original or Modified Versions of the Font Software may be bundled,
57
redistributed and/or sold with any software, provided that each copy
58
contains the above copyright notice and this license. These can be
59
included either as stand-alone text files, human-readable headers or
60
in the appropriate machine-readable metadata fields within text or
61
binary files as long as those fields can be easily viewed by the user.
62
63
3) No Modified Version of the Font Software may use the Reserved Font
64
Name(s) unless explicit written permission is granted by the corresponding
65
Copyright Holder. This restriction only applies to the primary font name as
66
presented to the users.
67
68
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
Software shall not be used to promote, endorse or advertise any
70
Modified Version, except to acknowledge the contribution(s) of the
71
Copyright Holder(s) and the Author(s) or with their explicit written
72
permission.
73
74
5) The Font Software, modified or unmodified, in part or in whole,
75
must be distributed entirely under this license, and must not be
76
distributed under any other license. The requirement for fonts to
77
remain under this license does not apply to any document created
78
using the Font Software.
79
80
TERMINATION
81
This license becomes null and void if any of the above conditions are
82
not met.
83
84
DISCLAIMER
85
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
OTHER DEALINGS IN THE FONT SOFTWARE.
194
M src/main/java/com/scrivenvar/Constants.java
9090
  public static final String DEFINITION_PROTOCOL_FILE = "file";
9191
92
  // Takes two parameters: line number and column number.
92
  // Three parameters: line number, column number, and offset
9393
  public static final String STATUS_BAR_LINE = "Main.statusbar.line";
9494
M src/main/java/com/scrivenvar/FileEditorTabPane.java
8888
      new ReadOnlyBooleanWrapper();
8989
  private final Consumer<Double> mScrollEventObserver;
90
  private final ChangeListener<Integer> mCaretListener;
9091
9192
  /**
9293
   * Constructs a new file editor tab pane.
9394
   */
94
  public FileEditorTabPane( final Consumer<Double> scrollEventObserver ) {
95
  public FileEditorTabPane(
96
      final Consumer<Double> scrollEventObserver,
97
      final ChangeListener<Integer> caretListener ) {
9598
    final ObservableList<Tab> tabs = getTabs();
9699
...
108111
    );
109112
110
    final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
111
                                                       newValue ) -> {
112
      for( final Tab tab : tabs ) {
113
        if( ((FileEditorTab) tab).isModified() ) {
114
          this.anyFileEditorModified.set( true );
115
          break;
116
        }
117
      }
118
    };
113
    final ChangeListener<Boolean> modifiedListener =
114
        ( observable, oldValue, newValue ) -> {
115
          for( final Tab tab : tabs ) {
116
            if( ((FileEditorTab) tab).isModified() ) {
117
              this.anyFileEditorModified.set( true );
118
              break;
119
            }
120
          }
121
        };
119122
120123
    tabs.addListener(
121124
        (ListChangeListener<Tab>) change -> {
122125
          while( change.next() ) {
123126
            if( change.wasAdded() ) {
124127
              change.getAddedSubList().forEach(
125
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
126
                                                  .addListener( modifiedListener ) );
128
                  ( tab ) ->
129
                      ((FileEditorTab) tab).modifiedProperty()
130
                                           .addListener( modifiedListener ) );
127131
            }
128132
            else if( change.wasRemoved() ) {
129133
              change.getRemoved().forEach(
130
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
131
                                                  .removeListener(
132
                                                      modifiedListener ) );
134
                  ( tab ) ->
135
                      ((FileEditorTab) tab).modifiedProperty()
136
                                           .removeListener( modifiedListener ) );
133137
            }
134138
          }
...
141145
142146
    mScrollEventObserver = scrollEventObserver;
147
    mCaretListener = caretListener;
143148
  }
144149
...
206211
      }
207212
    } );
213
214
    tab.addCaretParagraphListener( mCaretListener );
208215
209216
    return tab;
M src/main/java/com/scrivenvar/MainWindow.java
5151
import javafx.beans.property.BooleanProperty;
5252
import javafx.beans.property.SimpleBooleanProperty;
53
import javafx.beans.value.ObservableBooleanValue;
54
import javafx.beans.value.ObservableValue;
55
import javafx.collections.ListChangeListener.Change;
56
import javafx.collections.ObservableList;
57
import javafx.event.Event;
58
import javafx.event.EventHandler;
59
import javafx.geometry.Pos;
60
import javafx.scene.Node;
61
import javafx.scene.Scene;
62
import javafx.scene.control.*;
63
import javafx.scene.control.Alert.AlertType;
64
import javafx.scene.image.Image;
65
import javafx.scene.image.ImageView;
66
import javafx.scene.input.KeyEvent;
67
import javafx.scene.layout.BorderPane;
68
import javafx.scene.layout.VBox;
69
import javafx.scene.text.Text;
70
import javafx.stage.Window;
71
import javafx.stage.WindowEvent;
72
import javafx.util.Duration;
73
import org.controlsfx.control.StatusBar;
74
import org.fxmisc.richtext.model.TwoDimensional.Position;
75
76
import java.io.File;
77
import java.nio.file.Path;
78
import java.util.HashMap;
79
import java.util.Map;
80
import java.util.Observable;
81
import java.util.Observer;
82
import java.util.concurrent.atomic.AtomicInteger;
83
import java.util.function.Consumer;
84
import java.util.function.Function;
85
import java.util.prefs.Preferences;
86
87
import static com.scrivenvar.Constants.*;
88
import static com.scrivenvar.Messages.get;
89
import static com.scrivenvar.util.StageState.*;
90
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
91
import static javafx.event.Event.fireEvent;
92
import static javafx.scene.input.KeyCode.ENTER;
93
import static javafx.scene.input.KeyCode.TAB;
94
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
95
96
/**
97
 * Main window containing a tab pane in the center for file editors.
98
 *
99
 * @author Karl Tauber and White Magic Software, Ltd.
100
 */
101
public class MainWindow implements Observer {
102
103
  /**
104
   * The {@code OPTIONS} variable must be declared before all other variables
105
   * to prevent subsequent initializations from failing due to missing user
106
   * preferences.
107
   */
108
  private final static Options OPTIONS = Services.load( Options.class );
109
  private final static Snitch SNITCH = Services.load( Snitch.class );
110
  private final static Notifier NOTIFIER = Services.load( Notifier.class );
111
112
  private final Scene mScene;
113
  private final StatusBar mStatusBar;
114
  private final Text mLineNumberText;
115
  private final TextField mFindTextField;
116
117
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
118
  private final DefinitionPane mDefinitionPane = new DefinitionPane();
119
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
120
  private FileEditorTabPane fileEditorPane;
121
122
  /**
123
   * Prevents re-instantiation of processing classes.
124
   */
125
  private final Map<FileEditorTab, Processor<String>> mProcessors =
126
      new HashMap<>();
127
128
  private final Map<String, String> mResolvedMap =
129
      new HashMap<>( DEFAULT_MAP_SIZE );
130
131
  /**
132
   * Listens on the definition pane for double-click events.
133
   */
134
  private VariableNameInjector variableNameInjector;
135
136
  /**
137
   * Called when the definition data is changed.
138
   */
139
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
140
      mTreeHandler = event -> {
141
    exportDefinitions( getDefinitionPath() );
142
    interpolateResolvedMap();
143
    refreshActiveTab();
144
  };
145
146
  /**
147
   * Called to inject the selected item when the user presses ENTER in the
148
   * definition pane.
149
   */
150
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
151
      event -> {
152
        if( event.getCode() == ENTER ) {
153
          getVariableNameInjector().injectSelectedItem();
154
        }
155
      };
156
157
  /**
158
   * Called to switch to the definition pane when the user presses TAB.
159
   */
160
  private final EventHandler<? super KeyEvent> mEditorKeyHandler =
161
      (EventHandler<KeyEvent>) event -> {
162
        if( event.getCode() == TAB ) {
163
          getDefinitionPane().requestFocus();
164
          event.consume();
165
        }
166
      };
167
168
  private final Object mMutex = new Object();
169
  private final AtomicInteger mScrollRatio = new AtomicInteger( 0 );
170
171
  /**
172
   * Called to synchronize the scrolling areas.
173
   */
174
  private final Consumer<Double> mScrollEventObserver = o -> {
175
    final var eScrollPane = getActiveEditor().getScrollPane();
176
    final int eScrollY =
177
        eScrollPane.estimatedScrollYProperty().getValue().intValue();
178
    final int eHeight = (int)
179
        (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
180
            - eScrollPane.getHeight());
181
    final double eRatio = eHeight > 0
182
        ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
183
184
    final var pPreviewPane = getPreviewPane();
185
    final var pScrollBar = pPreviewPane.getVerticalScrollBar();
186
    final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
187
    final var pScrollY = (int) (pHeight * eRatio);
188
    final var pScrollPane = pPreviewPane.getScrollPane();
189
190
    final int oldScrollY = mScrollRatio.getAndSet( pScrollY );
191
    final int delta = Math.abs( oldScrollY - pScrollY );
192
193
    if( delta > 33 ) {
194
      // Prevent concurrent modification exceptions when attempting to
195
      // set the vertical scroll bar position.
196
      synchronized( mMutex ) {
197
        Platform.runLater( () -> {
198
          pScrollBar.setValue( pScrollY );
199
          pScrollPane.repaint();
200
        } );
201
      }
202
    }
203
  };
204
205
  public MainWindow() {
206
    mStatusBar = createStatusBar();
207
    mLineNumberText = createLineNumberText();
208
    mFindTextField = createFindTextField();
209
    mScene = createScene();
210
211
    initLayout();
212
    initFindInput();
213
    initSnitch();
214
    initDefinitionListener();
215
    initTabAddedListener();
216
    initTabChangedListener();
217
    restorePreferences();
218
  }
219
220
  private void initLayout() {
221
    final Scene appScene = getScene();
222
223
    appScene.getStylesheets().add( STYLESHEET_SCENE );
224
225
    // TODO: Apply an XML syntax highlighting for XML files.
226
//    appScene.getStylesheets().add( STYLESHEET_XML );
227
    appScene.windowProperty().addListener(
228
        ( observable, oldWindow, newWindow ) ->
229
            newWindow.setOnCloseRequest(
230
                e -> {
231
                  if( !getFileEditorPane().closeAllEditors() ) {
232
                    e.consume();
233
                  }
234
                }
235
            )
236
    );
237
  }
238
239
  /**
240
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
241
   * presses.
242
   */
243
  private void initFindInput() {
244
    final TextField input = getFindTextField();
245
246
    input.setOnKeyPressed( ( KeyEvent event ) -> {
247
      switch( event.getCode() ) {
248
        case F3:
249
        case ENTER:
250
          editFindNext();
251
          break;
252
        case F:
253
          if( !event.isControlDown() ) {
254
            break;
255
          }
256
        case ESCAPE:
257
          getStatusBar().setGraphic( null );
258
          getActiveFileEditor().getEditorPane().requestFocus();
259
          break;
260
      }
261
    } );
262
263
    // Remove when the input field loses focus.
264
    input.focusedProperty().addListener(
265
        (
266
            final ObservableValue<? extends Boolean> focused,
267
            final Boolean oFocus,
268
            final Boolean nFocus ) -> {
269
          if( !nFocus ) {
270
            getStatusBar().setGraphic( null );
271
          }
272
        }
273
    );
274
  }
275
276
  /**
277
   * Watch for changes to external files. In particular, this awaits
278
   * modifications to any XSL files associated with XML files being edited. When
279
   * an XSL file is modified (external to the application), the snitch's ears
280
   * perk up and the file is reloaded. This keeps the XSL transformation up to
281
   * date with what's on the file system.
282
   */
283
  private void initSnitch() {
284
    SNITCH.addObserver( this );
285
  }
286
287
  /**
288
   * Listen for {@link FileEditorTabPane} to receive open definition file event.
289
   */
290
  private void initDefinitionListener() {
291
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
292
        ( final ObservableValue<? extends Path> file,
293
          final Path oldPath, final Path newPath ) -> {
294
          // Indirectly refresh the resolved map.
295
          resetProcessors();
296
297
          openDefinitions( newPath );
298
299
          // Will create new processors and therefore a new resolved map.
300
          refreshActiveTab();
301
        }
302
    );
303
  }
304
305
  /**
306
   * When tabs are added, hook the various change listeners onto the new tab so
307
   * that the preview pane refreshes as necessary.
308
   */
309
  private void initTabAddedListener() {
310
    final FileEditorTabPane editorPane = getFileEditorPane();
311
312
    // Make sure the text processor kicks off when new files are opened.
313
    final ObservableList<Tab> tabs = editorPane.getTabs();
314
315
    // Update the preview pane on tab changes.
316
    tabs.addListener(
317
        ( final Change<? extends Tab> change ) -> {
318
          while( change.next() ) {
319
            if( change.wasAdded() ) {
320
              // Multiple tabs can be added simultaneously.
321
              for( final Tab newTab : change.getAddedSubList() ) {
322
                final FileEditorTab tab = (FileEditorTab) newTab;
323
324
                initTextChangeListener( tab );
325
                initKeyboardEventListeners( tab );
326
//              initSyntaxListener( tab );
327
              }
328
            }
329
          }
330
        }
331
    );
332
  }
333
334
  /**
335
   * Listen for new tab selection events.
336
   */
337
  private void initTabChangedListener() {
338
    final FileEditorTabPane editorPane = getFileEditorPane();
339
340
    // Update the preview pane changing tabs.
341
    editorPane.addTabSelectionListener(
342
        ( ObservableValue<? extends Tab> tabPane,
343
          final Tab oldTab, final Tab newTab ) -> {
344
          updateVariableNameInjector();
345
346
          // If there was no old tab, then this is a first time load, which
347
          // can be ignored.
348
          if( oldTab != null ) {
349
            if( newTab == null ) {
350
              closeRemainingTab();
351
            }
352
            else {
353
              // Update the preview with the edited text.
354
              refreshSelectedTab( (FileEditorTab) newTab );
355
            }
356
          }
357
        }
358
    );
359
  }
360
361
  /**
362
   * Reloads the preferences from the previous session.
363
   */
364
  private void restorePreferences() {
365
    restoreDefinitionPane();
366
    getFileEditorPane().restorePreferences();
367
  }
368
369
  /**
370
   * Ensure that the keyboard events are received when a new tab is added
371
   * to the user interface.
372
   *
373
   * @param tab The tab that can trigger keyboard events, such as control+space.
374
   */
375
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
376
    final VariableNameInjector vin = getVariableNameInjector();
377
    vin.initKeyboardEventListeners( tab );
378
379
    tab.addEventFilter( KeyEvent.KEY_PRESSED, mEditorKeyHandler );
380
  }
381
382
  private void initTextChangeListener( final FileEditorTab tab ) {
383
    tab.addTextChangeListener(
384
        ( ObservableValue<? extends String> editor,
385
          final String oldValue, final String newValue ) ->
386
            refreshSelectedTab( tab )
387
    );
388
  }
389
390
  private void updateVariableNameInjector() {
391
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
392
  }
393
394
  private void setVariableNameInjector( final VariableNameInjector injector ) {
395
    this.variableNameInjector = injector;
396
  }
397
398
  private synchronized VariableNameInjector getVariableNameInjector() {
399
    if( this.variableNameInjector == null ) {
400
      final VariableNameInjector vin = createVariableNameInjector();
401
      setVariableNameInjector( vin );
402
    }
403
404
    return this.variableNameInjector;
405
  }
406
407
  private VariableNameInjector createVariableNameInjector() {
408
    final FileEditorTab tab = getActiveFileEditor();
409
    final DefinitionPane pane = getDefinitionPane();
410
411
    return new VariableNameInjector( tab, pane );
412
  }
413
414
  /**
415
   * Called whenever the preview pane becomes out of sync with the file editor
416
   * tab. This can be called when the text changes, the caret paragraph changes,
417
   * or the file tab changes.
418
   *
419
   * @param tab The file editor tab that has been changed in some fashion.
420
   */
421
  private void refreshSelectedTab( final FileEditorTab tab ) {
422
    if( tab == null ) {
423
      return;
424
    }
425
426
    getPreviewPane().setPath( tab.getPath() );
427
428
    // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
429
    final Position p = tab.getCaretOffset();
430
    getLineNumberText().setText(
431
        get( STATUS_BAR_LINE,
432
             p.getMajor() + 1,
433
             p.getMinor() + 1,
434
             tab.getCaretPosition() + 1
435
        )
436
    );
437
438
    Processor<String> processor = getProcessors().get( tab );
439
440
    if( processor == null ) {
441
      processor = createProcessor( tab );
442
      getProcessors().put( tab, processor );
443
    }
444
445
    try {
446
      processor.processChain( tab.getEditorText() );
447
    } catch( final Exception ex ) {
448
      error( ex );
449
    }
450
  }
451
452
  private void refreshActiveTab() {
453
    refreshSelectedTab( getActiveFileEditor() );
454
  }
455
456
  /**
457
   * Called when a definition source is opened.
458
   *
459
   * @param path Path to the definition source that was opened.
460
   */
461
  private void openDefinitions( final Path path ) {
462
    try {
463
      final DefinitionSource ds = createDefinitionSource( path );
464
      setDefinitionSource( ds );
465
      getUserPreferences().definitionPathProperty().setValue( path.toFile() );
466
      getUserPreferences().save();
467
468
      final Tooltip tooltipPath = new Tooltip( path.toString() );
469
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
470
471
      final DefinitionPane pane = getDefinitionPane();
472
      pane.update( ds );
473
      pane.addTreeChangeHandler( mTreeHandler );
474
      pane.addKeyEventHandler( mDefinitionKeyHandler );
475
      pane.filenameProperty().setValue( path.getFileName().toString() );
476
      pane.setTooltip( tooltipPath );
477
478
      interpolateResolvedMap();
479
    } catch( final Exception e ) {
480
      error( e );
481
    }
482
  }
483
484
  private void exportDefinitions( final Path path ) {
485
    try {
486
      final DefinitionPane pane = getDefinitionPane();
487
      final TreeItem<String> root = pane.getTreeView().getRoot();
488
      final TreeItem<String> problemChild = pane.isTreeWellFormed();
489
490
      if( problemChild == null ) {
491
        getDefinitionSource().getTreeAdapter().export( root, path );
492
        getNotifier().clear();
493
      }
494
      else {
495
        final String msg = get( "yaml.error.tree.form",
496
                                problemChild.getValue() );
497
        getNotifier().notify( msg );
498
      }
499
    } catch( final Exception e ) {
500
      error( e );
501
    }
502
  }
503
504
  private void interpolateResolvedMap() {
505
    final Map<String, String> treeMap = getDefinitionPane().toMap();
506
    final Map<String, String> map = new HashMap<>( treeMap );
507
    MapInterpolator.interpolate( map );
508
509
    getResolvedMap().clear();
510
    getResolvedMap().putAll( map );
511
  }
512
513
  private void restoreDefinitionPane() {
514
    openDefinitions( getDefinitionPath() );
515
  }
516
517
  /**
518
   * Called when the last open tab is closed to clear the preview pane.
519
   */
520
  private void closeRemainingTab() {
521
    getPreviewPane().clear();
522
  }
523
524
  /**
525
   * Called when an exception occurs that warrants the user's attention.
526
   *
527
   * @param e The exception with a message that the user should know about.
528
   */
529
  private void error( final Exception e ) {
530
    getNotifier().notify( e );
531
  }
532
533
  //---- File actions -------------------------------------------------------
534
535
  /**
536
   * Called when an observable instance has changed. This is called by both the
537
   * snitch service and the notify service. The snitch service can be called for
538
   * different file types, including definition sources.
539
   *
540
   * @param observable The observed instance.
541
   * @param value      The noteworthy item.
542
   */
543
  @Override
544
  public void update( final Observable observable, final Object value ) {
545
    if( value != null ) {
546
      if( observable instanceof Snitch && value instanceof Path ) {
547
        updateSelectedTab();
548
      }
549
      else if( observable instanceof Notifier && value instanceof String ) {
550
        updateStatusBar( (String) value );
551
      }
552
    }
553
  }
554
555
  /**
556
   * Updates the status bar to show the given message.
557
   *
558
   * @param s The message to show in the status bar.
559
   */
560
  private void updateStatusBar( final String s ) {
561
    Platform.runLater(
562
        () -> {
563
          final int index = s.indexOf( '\n' );
564
          final String message = s.substring(
565
              0, index > 0 ? index : s.length() );
566
567
          getStatusBar().setText( message );
568
        }
569
    );
570
  }
571
572
  /**
573
   * Called when a file has been modified.
574
   */
575
  private void updateSelectedTab() {
576
    Platform.runLater(
577
        () -> {
578
          // Brute-force XSLT file reload by re-instantiating all processors.
579
          resetProcessors();
580
          refreshActiveTab();
581
        }
582
    );
583
  }
584
585
  /**
586
   * After resetting the processors, they will refresh anew to be up-to-date
587
   * with the files (text and definition) currently loaded into the editor.
588
   */
589
  private void resetProcessors() {
590
    getProcessors().clear();
591
  }
592
593
  //---- File actions -------------------------------------------------------
594
595
  private void fileNew() {
596
    getFileEditorPane().newEditor();
597
  }
598
599
  private void fileOpen() {
600
    getFileEditorPane().openFileDialog();
601
  }
602
603
  private void fileClose() {
604
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
605
  }
606
607
  /**
608
   * TODO: Upon closing, first remove the tab change listeners. (There's no
609
   * need to re-render each tab when all are being closed.)
610
   */
611
  private void fileCloseAll() {
612
    getFileEditorPane().closeAllEditors();
613
  }
614
615
  private void fileSave() {
616
    getFileEditorPane().saveEditor( getActiveFileEditor() );
617
  }
618
619
  private void fileSaveAs() {
620
    final FileEditorTab editor = getActiveFileEditor();
621
    getFileEditorPane().saveEditorAs( editor );
622
    getProcessors().remove( editor );
623
624
    try {
625
      refreshSelectedTab( editor );
626
    } catch( final Exception ex ) {
627
      getNotifier().notify( ex );
628
    }
629
  }
630
631
  private void fileSaveAll() {
632
    getFileEditorPane().saveAllEditors();
633
  }
634
635
  private void fileExit() {
636
    final Window window = getWindow();
637
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
638
  }
639
640
  //---- Edit actions -------------------------------------------------------
641
642
  /**
643
   * Used to find text in the active file editor window.
644
   */
645
  private void editFind() {
646
    final TextField input = getFindTextField();
647
    getStatusBar().setGraphic( input );
648
    input.requestFocus();
649
  }
650
651
  public void editFindNext() {
652
    getActiveFileEditor().searchNext( getFindTextField().getText() );
653
  }
654
655
  public void editPreferences() {
656
    getUserPreferences().show();
657
  }
658
659
  //---- Insert actions -----------------------------------------------------
660
661
  /**
662
   * Delegates to the active editor to handle wrapping the current text
663
   * selection with leading and trailing strings.
664
   *
665
   * @param leading  The string to put before the selection.
666
   * @param trailing The string to put after the selection.
667
   */
668
  private void insertMarkdown(
669
      final String leading, final String trailing ) {
670
    getActiveEditor().surroundSelection( leading, trailing );
671
  }
672
673
  @SuppressWarnings("SameParameterValue")
674
  private void insertMarkdown(
675
      final String leading, final String trailing, final String hint ) {
676
    getActiveEditor().surroundSelection( leading, trailing, hint );
677
  }
678
679
  //---- Help actions -------------------------------------------------------
680
681
  private void helpAbout() {
682
    final Alert alert = new Alert( AlertType.INFORMATION );
683
    alert.setTitle( get( "Dialog.about.title" ) );
684
    alert.setHeaderText( get( "Dialog.about.header" ) );
685
    alert.setContentText( get( "Dialog.about.content" ) );
686
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
687
    alert.initOwner( getWindow() );
688
689
    alert.showAndWait();
690
  }
691
692
  //---- Member creators ----------------------------------------------------
693
694
  /**
695
   * Factory to create processors that are suited to different file types.
696
   *
697
   * @param tab The tab that is subjected to processing.
698
   * @return A processor suited to the file type specified by the tab's path.
699
   */
700
  private Processor<String> createProcessor( final FileEditorTab tab ) {
701
    return createProcessorFactory().createProcessor( tab );
702
  }
703
704
  private ProcessorFactory createProcessorFactory() {
705
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
706
  }
707
708
  private HTMLPreviewPane createHTMLPreviewPane() {
709
    return new HTMLPreviewPane();
710
  }
711
712
  private DefinitionSource createDefaultDefinitionSource() {
713
    return new YamlDefinitionSource( getDefinitionPath() );
714
  }
715
716
  private DefinitionSource createDefinitionSource( final Path path ) {
717
    try {
718
      return createDefinitionFactory().createDefinitionSource( path );
719
    } catch( final Exception ex ) {
720
      error( ex );
721
      return createDefaultDefinitionSource();
722
    }
723
  }
724
725
  private TextField createFindTextField() {
726
    return new TextField();
727
  }
728
729
  /**
730
   * Create an editor pane to hold file editor tabs.
731
   *
732
   * @return A new instance, never null.
733
   */
734
  private FileEditorTabPane createFileEditorPane() {
735
    return new FileEditorTabPane( mScrollEventObserver );
736
  }
737
738
  private DefinitionFactory createDefinitionFactory() {
739
    return new DefinitionFactory();
740
  }
741
742
  private StatusBar createStatusBar() {
743
    return new StatusBar();
744
  }
745
746
  private Scene createScene() {
747
    final SplitPane splitPane = new SplitPane(
748
        getDefinitionPane().getNode(),
749
        getFileEditorPane().getNode(),
750
        getPreviewPane().getNode() );
751
752
    splitPane.setDividerPositions(
753
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
754
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
755
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
756
757
    getDefinitionPane().prefHeightProperty().bind( splitPane.heightProperty() );
758
759
    final BorderPane borderPane = new BorderPane();
760
    borderPane.setPrefSize( 1024, 800 );
761
    borderPane.setTop( createMenuBar() );
762
    borderPane.setBottom( getStatusBar() );
763
    borderPane.setCenter( splitPane );
764
765
    final VBox statusBar = new VBox();
766
    statusBar.setAlignment( Pos.BASELINE_CENTER );
767
    statusBar.getChildren().add( getLineNumberText() );
768
    getStatusBar().getRightItems().add( statusBar );
769
770
    return new Scene( borderPane );
771
  }
772
773
  private Text createLineNumberText() {
774
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
775
  }
776
777
  private Node createMenuBar() {
778
    final BooleanBinding activeFileEditorIsNull =
779
        getFileEditorPane().activeFileEditorProperty().isNull();
780
781
    // File actions
782
    final Action fileNewAction = new ActionBuilder()
783
        .setText( "Main.menu.file.new" )
784
        .setAccelerator( "Shortcut+N" )
785
        .setIcon( FILE_ALT )
786
        .setAction( e -> fileNew() )
787
        .build();
788
    final Action fileOpenAction = new ActionBuilder()
789
        .setText( "Main.menu.file.open" )
790
        .setAccelerator( "Shortcut+O" )
791
        .setIcon( FOLDER_OPEN_ALT )
792
        .setAction( e -> fileOpen() )
793
        .build();
794
    final Action fileCloseAction = new ActionBuilder()
795
        .setText( "Main.menu.file.close" )
796
        .setAccelerator( "Shortcut+W" )
797
        .setAction( e -> fileClose() )
798
        .setDisable( activeFileEditorIsNull )
799
        .build();
800
    final Action fileCloseAllAction = new ActionBuilder()
801
        .setText( "Main.menu.file.close_all" )
802
        .setAction( e -> fileCloseAll() )
803
        .setDisable( activeFileEditorIsNull )
804
        .build();
805
    final Action fileSaveAction = new ActionBuilder()
806
        .setText( "Main.menu.file.save" )
807
        .setAccelerator( "Shortcut+S" )
808
        .setIcon( FLOPPY_ALT )
809
        .setAction( e -> fileSave() )
810
        .setDisable( createActiveBooleanProperty(
811
            FileEditorTab::modifiedProperty ).not() )
812
        .build();
813
    final Action fileSaveAsAction = new ActionBuilder()
814
        .setText( "Main.menu.file.save_as" )
815
        .setAction( e -> fileSaveAs() )
816
        .setDisable( activeFileEditorIsNull )
817
        .build();
818
    final Action fileSaveAllAction = new ActionBuilder()
819
        .setText( "Main.menu.file.save_all" )
820
        .setAccelerator( "Shortcut+Shift+S" )
821
        .setAction( e -> fileSaveAll() )
822
        .setDisable( Bindings.not(
823
            getFileEditorPane().anyFileEditorModifiedProperty() ) )
824
        .build();
825
    final Action fileExitAction = new ActionBuilder()
826
        .setText( "Main.menu.file.exit" )
827
        .setAction( e -> fileExit() )
828
        .build();
829
830
    // Edit actions
831
    final Action editUndoAction = new ActionBuilder()
832
        .setText( "Main.menu.edit.undo" )
833
        .setAccelerator( "Shortcut+Z" )
834
        .setIcon( UNDO )
835
        .setAction( e -> getActiveEditor().undo() )
836
        .setDisable( createActiveBooleanProperty(
837
            FileEditorTab::canUndoProperty ).not() )
838
        .build();
839
    final Action editRedoAction = new ActionBuilder()
840
        .setText( "Main.menu.edit.redo" )
841
        .setAccelerator( "Shortcut+Y" )
842
        .setIcon( REPEAT )
843
        .setAction( e -> getActiveEditor().redo() )
844
        .setDisable( createActiveBooleanProperty(
845
            FileEditorTab::canRedoProperty ).not() )
846
        .build();
847
    final Action editFindAction = new ActionBuilder()
848
        .setText( "Main.menu.edit.find" )
849
        .setAccelerator( "Ctrl+F" )
850
        .setIcon( SEARCH )
851
        .setAction( e -> editFind() )
852
        .setDisable( activeFileEditorIsNull )
853
        .build();
854
    final Action editFindNextAction = new ActionBuilder()
855
        .setText( "Main.menu.edit.find.next" )
856
        .setAccelerator( "F3" )
857
        .setIcon( null )
858
        .setAction( e -> editFindNext() )
859
        .setDisable( activeFileEditorIsNull )
860
        .build();
861
    final Action editPreferencesAction = new ActionBuilder()
862
        .setText( "Main.menu.edit.preferences" )
863
        .setAccelerator( "Ctrl+Alt+S" )
864
        .setAction( e -> editPreferences() )
865
        .build();
866
867
    // Insert actions
868
    final Action insertBoldAction = new ActionBuilder()
869
        .setText( "Main.menu.insert.bold" )
870
        .setAccelerator( "Shortcut+B" )
871
        .setIcon( BOLD )
872
        .setAction( e -> insertMarkdown( "**", "**" ) )
873
        .setDisable( activeFileEditorIsNull )
874
        .build();
875
    final Action insertItalicAction = new ActionBuilder()
876
        .setText( "Main.menu.insert.italic" )
877
        .setAccelerator( "Shortcut+I" )
878
        .setIcon( ITALIC )
879
        .setAction( e -> insertMarkdown( "*", "*" ) )
880
        .setDisable( activeFileEditorIsNull )
881
        .build();
882
    final Action insertSuperscriptAction = new ActionBuilder()
883
        .setText( "Main.menu.insert.superscript" )
884
        .setAccelerator( "Shortcut+[" )
885
        .setIcon( SUPERSCRIPT )
886
        .setAction( e -> insertMarkdown( "^", "^" ) )
887
        .setDisable( activeFileEditorIsNull )
888
        .build();
889
    final Action insertSubscriptAction = new ActionBuilder()
890
        .setText( "Main.menu.insert.subscript" )
891
        .setAccelerator( "Shortcut+]" )
892
        .setIcon( SUBSCRIPT )
893
        .setAction( e -> insertMarkdown( "~", "~" ) )
894
        .setDisable( activeFileEditorIsNull )
895
        .build();
896
    final Action insertStrikethroughAction = new ActionBuilder()
897
        .setText( "Main.menu.insert.strikethrough" )
898
        .setAccelerator( "Shortcut+T" )
899
        .setIcon( STRIKETHROUGH )
900
        .setAction( e -> insertMarkdown( "~~", "~~" ) )
901
        .setDisable( activeFileEditorIsNull )
902
        .build();
903
    final Action insertBlockquoteAction = new ActionBuilder()
904
        .setText( "Main.menu.insert.blockquote" )
905
        .setAccelerator( "Ctrl+Q" )
906
        .setIcon( QUOTE_LEFT )
907
        .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
908
        .setDisable( activeFileEditorIsNull )
909
        .build();
910
    final Action insertCodeAction = new ActionBuilder()
911
        .setText( "Main.menu.insert.code" )
912
        .setAccelerator( "Shortcut+K" )
913
        .setIcon( CODE )
914
        .setAction( e -> insertMarkdown( "`", "`" ) )
915
        .setDisable( activeFileEditorIsNull )
916
        .build();
917
    final Action insertFencedCodeBlockAction = new ActionBuilder()
918
        .setText( "Main.menu.insert.fenced_code_block" )
919
        .setAccelerator( "Shortcut+Shift+K" )
920
        .setIcon( FILE_CODE_ALT )
921
        .setAction( e -> getActiveEditor().surroundSelection(
922
            "\n\n```\n",
923
            "\n```\n\n",
924
            get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
925
        .setDisable( activeFileEditorIsNull )
926
        .build();
927
    final Action insertLinkAction = new ActionBuilder()
928
        .setText( "Main.menu.insert.link" )
929
        .setAccelerator( "Shortcut+L" )
930
        .setIcon( LINK )
931
        .setAction( e -> getActiveEditor().insertLink() )
932
        .setDisable( activeFileEditorIsNull )
933
        .build();
934
    final Action insertImageAction = new ActionBuilder()
935
        .setText( "Main.menu.insert.image" )
936
        .setAccelerator( "Shortcut+G" )
937
        .setIcon( PICTURE_ALT )
938
        .setAction( e -> getActiveEditor().insertImage() )
939
        .setDisable( activeFileEditorIsNull )
940
        .build();
941
942
    // Number of header actions (H1 ... H3)
943
    final int HEADERS = 3;
944
    final Action[] headers = new Action[ HEADERS ];
945
946
    for( int i = 1; i <= HEADERS; i++ ) {
947
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
948
      final String markup = String.format( "%n%n%s ", hashes );
949
      final String text = "Main.menu.insert.header." + i;
950
      final String accelerator = "Shortcut+" + i;
951
      final String prompt = text + ".prompt";
952
953
      headers[ i - 1 ] = new ActionBuilder()
954
          .setText( text )
955
          .setAccelerator( accelerator )
956
          .setIcon( HEADER )
957
          .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
958
          .setDisable( activeFileEditorIsNull )
959
          .build();
960
    }
961
962
    final Action insertUnorderedListAction = new ActionBuilder()
963
        .setText( "Main.menu.insert.unordered_list" )
964
        .setAccelerator( "Shortcut+U" )
965
        .setIcon( LIST_UL )
966
        .setAction( e -> getActiveEditor()
967
            .surroundSelection( "\n\n* ", "" ) )
968
        .setDisable( activeFileEditorIsNull )
969
        .build();
970
    final Action insertOrderedListAction = new ActionBuilder()
971
        .setText( "Main.menu.insert.ordered_list" )
972
        .setAccelerator( "Shortcut+Shift+O" )
973
        .setIcon( LIST_OL )
974
        .setAction( e -> insertMarkdown(
975
            "\n\n1. ", "" ) )
976
        .setDisable( activeFileEditorIsNull )
977
        .build();
978
    final Action insertHorizontalRuleAction = new ActionBuilder()
979
        .setText( "Main.menu.insert.horizontal_rule" )
980
        .setAccelerator( "Shortcut+H" )
981
        .setAction( e -> insertMarkdown(
982
            "\n\n---\n\n", "" ) )
983
        .setDisable( activeFileEditorIsNull )
984
        .build();
985
986
    // Help actions
987
    final Action helpAboutAction = new ActionBuilder()
988
        .setText( "Main.menu.help.about" )
989
        .setAction( e -> helpAbout() )
990
        .build();
991
992
    //---- MenuBar ----
993
    final Menu fileMenu = ActionUtils.createMenu(
994
        get( "Main.menu.file" ),
995
        fileNewAction,
996
        fileOpenAction,
997
        null,
998
        fileCloseAction,
999
        fileCloseAllAction,
1000
        null,
1001
        fileSaveAction,
1002
        fileSaveAsAction,
1003
        fileSaveAllAction,
1004
        null,
1005
        fileExitAction );
1006
1007
    final Menu editMenu = ActionUtils.createMenu(
1008
        get( "Main.menu.edit" ),
1009
        editUndoAction,
1010
        editRedoAction,
1011
        editFindAction,
1012
        editFindNextAction,
1013
        null,
1014
        editPreferencesAction );
1015
1016
    final Menu insertMenu = ActionUtils.createMenu(
1017
        get( "Main.menu.insert" ),
1018
        insertBoldAction,
1019
        insertItalicAction,
1020
        insertSuperscriptAction,
1021
        insertSubscriptAction,
1022
        insertStrikethroughAction,
1023
        insertBlockquoteAction,
1024
        insertCodeAction,
1025
        insertFencedCodeBlockAction,
1026
        null,
1027
        insertLinkAction,
1028
        insertImageAction,
1029
        null,
1030
        headers[ 0 ],
1031
        headers[ 1 ],
1032
        headers[ 2 ],
1033
        null,
1034
        insertUnorderedListAction,
1035
        insertOrderedListAction,
1036
        insertHorizontalRuleAction );
1037
1038
    final Menu helpMenu = ActionUtils.createMenu(
1039
        get( "Main.menu.help" ),
1040
        helpAboutAction );
1041
1042
    final MenuBar menuBar = new MenuBar(
1043
        fileMenu,
1044
        editMenu,
1045
        insertMenu,
1046
        helpMenu );
1047
1048
    //---- ToolBar ----
1049
    final ToolBar toolBar = ActionUtils.createToolBar(
1050
        fileNewAction,
1051
        fileOpenAction,
1052
        fileSaveAction,
1053
        null,
1054
        editUndoAction,
1055
        editRedoAction,
1056
        null,
1057
        insertBoldAction,
1058
        insertItalicAction,
1059
        insertSuperscriptAction,
1060
        insertSubscriptAction,
1061
        insertBlockquoteAction,
1062
        insertCodeAction,
1063
        insertFencedCodeBlockAction,
1064
        null,
1065
        insertLinkAction,
1066
        insertImageAction,
1067
        null,
1068
        headers[ 0 ],
1069
        null,
1070
        insertUnorderedListAction,
1071
        insertOrderedListAction );
1072
1073
    return new VBox( menuBar, toolBar );
1074
  }
1075
1076
  private UserPreferences createUserPreferences() {
1077
    return new UserPreferences();
1078
  }
1079
1080
  /**
1081
   * Creates a boolean property that is bound to another boolean value of the
1082
   * active editor.
1083
   */
1084
  private BooleanProperty createActiveBooleanProperty(
1085
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1086
1087
    final BooleanProperty b = new SimpleBooleanProperty();
1088
    final FileEditorTab tab = getActiveFileEditor();
1089
1090
    if( tab != null ) {
1091
      b.bind( func.apply( tab ) );
1092
    }
1093
1094
    getFileEditorPane().activeFileEditorProperty().addListener(
1095
        ( observable, oldFileEditor, newFileEditor ) -> {
1096
          b.unbind();
1097
1098
          if( newFileEditor == null ) {
1099
            b.set( false );
1100
          }
1101
          else {
1102
            b.bind( func.apply( newFileEditor ) );
1103
          }
1104
        }
1105
    );
1106
1107
    return b;
1108
  }
1109
1110
  //---- Convenience accessors ----------------------------------------------
1111
1112
  private Preferences getPreferences() {
1113
    return OPTIONS.getState();
1114
  }
1115
1116
  private float getFloat( final String key, final float defaultValue ) {
1117
    return getPreferences().getFloat( key, defaultValue );
1118
  }
1119
1120
  public Window getWindow() {
1121
    return getScene().getWindow();
1122
  }
1123
1124
  private MarkdownEditorPane getActiveEditor() {
1125
    final EditorPane pane = getActiveFileEditor().getEditorPane();
1126
1127
    return pane instanceof MarkdownEditorPane
1128
        ? (MarkdownEditorPane) pane
1129
        : new MarkdownEditorPane();
1130
  }
1131
1132
  private FileEditorTab getActiveFileEditor() {
1133
    return getFileEditorPane().getActiveFileEditor();
1134
  }
1135
1136
  //---- Member accessors ---------------------------------------------------
1137
1138
  protected Scene getScene() {
1139
    return mScene;
1140
  }
1141
1142
  private Map<FileEditorTab, Processor<String>> getProcessors() {
1143
    return mProcessors;
1144
  }
1145
1146
  private FileEditorTabPane getFileEditorPane() {
1147
    if( this.fileEditorPane == null ) {
1148
      this.fileEditorPane = createFileEditorPane();
1149
    }
1150
1151
    return this.fileEditorPane;
1152
  }
1153
1154
  private HTMLPreviewPane getPreviewPane() {
1155
    return mPreviewPane;
1156
  }
1157
1158
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
1159
    assert definitionSource != null;
1160
    mDefinitionSource = definitionSource;
1161
  }
1162
1163
  private DefinitionSource getDefinitionSource() {
1164
    return mDefinitionSource;
1165
  }
1166
1167
  private DefinitionPane getDefinitionPane() {
1168
    return mDefinitionPane;
1169
  }
1170
1171
  private Notifier getNotifier() {
1172
    return NOTIFIER;
1173
  }
1174
1175
  private Text getLineNumberText() {
1176
    return mLineNumberText;
1177
  }
1178
1179
  private StatusBar getStatusBar() {
1180
    return mStatusBar;
1181
  }
1182
1183
  private TextField getFindTextField() {
1184
    return mFindTextField;
1185
  }
1186
1187
  /**
1188
   * Returns the variable map of interpolated definitions.
1189
   *
1190
   * @return A map to help dereference variables.
1191
   */
1192
  private Map<String, String> getResolvedMap() {
1193
    return mResolvedMap;
1194
  }
1195
1196
  //---- Persistence accessors ----------------------------------------------
1197
  private UserPreferences getUserPreferences() {
1198
    return OPTIONS.getUserPreferences();
1199
  }
1200
1201
  private Path getDefinitionPath() {
1202
    return getUserPreferences().getDefinitionPath();
1203
  }
1204
1205
  private File getImagesDirectory() {
1206
    return getUserPreferences().getImagesDirectory();
1207
  }
1208
1209
  private String getImagesOrder() {
1210
    return getUserPreferences().getImagesOrder();
53
import javafx.beans.value.ChangeListener;
54
import javafx.beans.value.ObservableBooleanValue;
55
import javafx.beans.value.ObservableValue;
56
import javafx.collections.ListChangeListener.Change;
57
import javafx.collections.ObservableList;
58
import javafx.event.Event;
59
import javafx.event.EventHandler;
60
import javafx.geometry.Pos;
61
import javafx.scene.Node;
62
import javafx.scene.Scene;
63
import javafx.scene.control.*;
64
import javafx.scene.control.Alert.AlertType;
65
import javafx.scene.image.Image;
66
import javafx.scene.image.ImageView;
67
import javafx.scene.input.KeyEvent;
68
import javafx.scene.layout.BorderPane;
69
import javafx.scene.layout.VBox;
70
import javafx.scene.text.Text;
71
import javafx.stage.Window;
72
import javafx.stage.WindowEvent;
73
import javafx.util.Duration;
74
import org.controlsfx.control.StatusBar;
75
import org.fxmisc.richtext.StyleClassedTextArea;
76
import org.fxmisc.richtext.model.TwoDimensional;
77
78
import java.io.File;
79
import java.nio.file.Path;
80
import java.util.HashMap;
81
import java.util.Map;
82
import java.util.Observable;
83
import java.util.Observer;
84
import java.util.concurrent.atomic.AtomicInteger;
85
import java.util.function.Consumer;
86
import java.util.function.Function;
87
import java.util.prefs.Preferences;
88
89
import static com.scrivenvar.Constants.*;
90
import static com.scrivenvar.Messages.get;
91
import static com.scrivenvar.util.StageState.*;
92
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
93
import static javafx.event.Event.fireEvent;
94
import static javafx.scene.input.KeyCode.ENTER;
95
import static javafx.scene.input.KeyCode.TAB;
96
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
97
98
/**
99
 * Main window containing a tab pane in the center for file editors.
100
 *
101
 * @author Karl Tauber and White Magic Software, Ltd.
102
 */
103
public class MainWindow implements Observer {
104
105
  /**
106
   * The {@code OPTIONS} variable must be declared before all other variables
107
   * to prevent subsequent initializations from failing due to missing user
108
   * preferences.
109
   */
110
  private final static Options OPTIONS = Services.load( Options.class );
111
  private final static Snitch SNITCH = Services.load( Snitch.class );
112
  private final static Notifier NOTIFIER = Services.load( Notifier.class );
113
114
  private final Scene mScene;
115
  private final StatusBar mStatusBar;
116
  private final Text mLineNumberText;
117
  private final TextField mFindTextField;
118
119
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
120
  private final DefinitionPane mDefinitionPane = new DefinitionPane();
121
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
122
  private FileEditorTabPane mFileEditorPane;
123
124
  /**
125
   * Prevents re-instantiation of processing classes.
126
   */
127
  private final Map<FileEditorTab, Processor<String>> mProcessors =
128
      new HashMap<>();
129
130
  private final Map<String, String> mResolvedMap =
131
      new HashMap<>( DEFAULT_MAP_SIZE );
132
133
  /**
134
   * Listens on the definition pane for double-click events.
135
   */
136
  private VariableNameInjector variableNameInjector;
137
138
  /**
139
   * Called when the definition data is changed.
140
   */
141
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
142
      mTreeHandler = event -> {
143
    exportDefinitions( getDefinitionPath() );
144
    interpolateResolvedMap();
145
    refreshActiveTab();
146
  };
147
148
  /**
149
   * Called to inject the selected item when the user presses ENTER in the
150
   * definition pane.
151
   */
152
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
153
      event -> {
154
        if( event.getCode() == ENTER ) {
155
          getVariableNameInjector().injectSelectedItem();
156
        }
157
      };
158
159
  /**
160
   * Called to switch to the definition pane when the user presses TAB.
161
   */
162
  private final EventHandler<? super KeyEvent> mEditorKeyHandler =
163
      (EventHandler<KeyEvent>) event -> {
164
        if( event.getCode() == TAB ) {
165
          getDefinitionPane().requestFocus();
166
          event.consume();
167
        }
168
      };
169
170
  private final Object mMutex = new Object();
171
  private final AtomicInteger mScrollRatio = new AtomicInteger( 0 );
172
173
  /**
174
   * Called to synchronize the scrolling areas.
175
   */
176
  private final Consumer<Double> mScrollEventObserver = o -> {
177
    final boolean scrolling = false;
178
    final var pPreviewPane = getPreviewPane();
179
    final var pScrollPane = pPreviewPane.getScrollPane();
180
181
    // If the user is deliberately using the scrollbar then synchronize
182
    // them by calculating the ratios.
183
    if( scrolling ) {
184
      final var eScrollPane = getActiveEditor().getScrollPane();
185
      final int eScrollY =
186
          eScrollPane.estimatedScrollYProperty().getValue().intValue();
187
      final int eHeight = (int)
188
          (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
189
              - eScrollPane.getHeight());
190
      final double eRatio = eHeight > 0
191
          ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
192
193
      final var pScrollBar = pPreviewPane.getVerticalScrollBar();
194
      final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
195
      final var pScrollY = (int) (pHeight * eRatio);
196
197
      // Reduce concurrent modification exceptions when setting the vertical
198
      // scroll bar position.
199
      synchronized( mMutex ) {
200
        Platform.runLater( () -> {
201
          pScrollBar.setValue( pScrollY );
202
          pScrollPane.repaint();
203
        } );
204
      }
205
    }
206
    else {
207
      synchronized( mMutex ) {
208
        Platform.runLater( () -> {
209
          final String id = getActiveEditor().getCurrentParagraphId();
210
          pPreviewPane.scrollTo( id );
211
          pScrollPane.repaint();
212
        } );
213
      }
214
    }
215
  };
216
217
  private final ChangeListener<Integer> mCaretListener = ( i, j, k ) -> {
218
    final FileEditorTab tab = getActiveFileEditor();
219
    final EditorPane pane = tab.getEditorPane();
220
    final StyleClassedTextArea editor = pane.getEditor();
221
222
    getLineNumberText().setText(
223
        get( STATUS_BAR_LINE,
224
             editor.getCurrentParagraph() + 1,
225
             editor.getParagraphs().size(),
226
             editor.getCaretPosition()
227
        )
228
    );
229
  };
230
231
  public MainWindow() {
232
    mStatusBar = createStatusBar();
233
    mLineNumberText = createLineNumberText();
234
    mFindTextField = createFindTextField();
235
    mScene = createScene();
236
237
    initLayout();
238
    initFindInput();
239
    initSnitch();
240
    initDefinitionListener();
241
    initTabAddedListener();
242
    initTabChangedListener();
243
    restorePreferences();
244
  }
245
246
  private void initLayout() {
247
    final Scene appScene = getScene();
248
249
    appScene.getStylesheets().add( STYLESHEET_SCENE );
250
251
    // TODO: Apply an XML syntax highlighting for XML files.
252
//    appScene.getStylesheets().add( STYLESHEET_XML );
253
    appScene.windowProperty().addListener(
254
        ( observable, oldWindow, newWindow ) ->
255
            newWindow.setOnCloseRequest(
256
                e -> {
257
                  if( !getFileEditorPane().closeAllEditors() ) {
258
                    e.consume();
259
                  }
260
                }
261
            )
262
    );
263
  }
264
265
  /**
266
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
267
   * presses.
268
   */
269
  private void initFindInput() {
270
    final TextField input = getFindTextField();
271
272
    input.setOnKeyPressed( ( KeyEvent event ) -> {
273
      switch( event.getCode() ) {
274
        case F3:
275
        case ENTER:
276
          editFindNext();
277
          break;
278
        case F:
279
          if( !event.isControlDown() ) {
280
            break;
281
          }
282
        case ESCAPE:
283
          getStatusBar().setGraphic( null );
284
          getActiveFileEditor().getEditorPane().requestFocus();
285
          break;
286
      }
287
    } );
288
289
    // Remove when the input field loses focus.
290
    input.focusedProperty().addListener(
291
        (
292
            final ObservableValue<? extends Boolean> focused,
293
            final Boolean oFocus,
294
            final Boolean nFocus ) -> {
295
          if( !nFocus ) {
296
            getStatusBar().setGraphic( null );
297
          }
298
        }
299
    );
300
  }
301
302
  /**
303
   * Watch for changes to external files. In particular, this awaits
304
   * modifications to any XSL files associated with XML files being edited. When
305
   * an XSL file is modified (external to the application), the snitch's ears
306
   * perk up and the file is reloaded. This keeps the XSL transformation up to
307
   * date with what's on the file system.
308
   */
309
  private void initSnitch() {
310
    SNITCH.addObserver( this );
311
  }
312
313
  /**
314
   * Listen for {@link FileEditorTabPane} to receive open definition file event.
315
   */
316
  private void initDefinitionListener() {
317
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
318
        ( final ObservableValue<? extends Path> file,
319
          final Path oldPath, final Path newPath ) -> {
320
          // Indirectly refresh the resolved map.
321
          resetProcessors();
322
323
          openDefinitions( newPath );
324
325
          // Will create new processors and therefore a new resolved map.
326
          refreshActiveTab();
327
        }
328
    );
329
  }
330
331
  /**
332
   * When tabs are added, hook the various change listeners onto the new tab so
333
   * that the preview pane refreshes as necessary.
334
   */
335
  private void initTabAddedListener() {
336
    final FileEditorTabPane editorPane = getFileEditorPane();
337
338
    // Make sure the text processor kicks off when new files are opened.
339
    final ObservableList<Tab> tabs = editorPane.getTabs();
340
341
    // Update the preview pane on tab changes.
342
    tabs.addListener(
343
        ( final Change<? extends Tab> change ) -> {
344
          while( change.next() ) {
345
            if( change.wasAdded() ) {
346
              // Multiple tabs can be added simultaneously.
347
              for( final Tab newTab : change.getAddedSubList() ) {
348
                final FileEditorTab tab = (FileEditorTab) newTab;
349
350
                initTextChangeListener( tab );
351
                initKeyboardEventListeners( tab );
352
//              initSyntaxListener( tab );
353
              }
354
            }
355
          }
356
        }
357
    );
358
  }
359
360
  /**
361
   * Listen for new tab selection events.
362
   */
363
  private void initTabChangedListener() {
364
    final FileEditorTabPane editorPane = getFileEditorPane();
365
366
    // Update the preview pane changing tabs.
367
    editorPane.addTabSelectionListener(
368
        ( ObservableValue<? extends Tab> tabPane,
369
          final Tab oldTab, final Tab newTab ) -> {
370
          updateVariableNameInjector();
371
372
          // If there was no old tab, then this is a first time load, which
373
          // can be ignored.
374
          if( oldTab != null ) {
375
            if( newTab == null ) {
376
              closeRemainingTab();
377
            }
378
            else {
379
              // Update the preview with the edited text.
380
              refreshSelectedTab( (FileEditorTab) newTab );
381
            }
382
          }
383
        }
384
    );
385
  }
386
387
  /**
388
   * Reloads the preferences from the previous session.
389
   */
390
  private void restorePreferences() {
391
    restoreDefinitionPane();
392
    getFileEditorPane().restorePreferences();
393
  }
394
395
  /**
396
   * Ensure that the keyboard events are received when a new tab is added
397
   * to the user interface.
398
   *
399
   * @param tab The tab that can trigger keyboard events, such as control+space.
400
   */
401
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
402
    final VariableNameInjector vin = getVariableNameInjector();
403
    vin.initKeyboardEventListeners( tab );
404
405
    tab.addEventFilter( KeyEvent.KEY_PRESSED, mEditorKeyHandler );
406
  }
407
408
  private void initTextChangeListener( final FileEditorTab tab ) {
409
    tab.addTextChangeListener(
410
        ( ObservableValue<? extends String> editor,
411
          final String oldValue, final String newValue ) ->
412
            refreshSelectedTab( tab )
413
    );
414
  }
415
416
  private void updateVariableNameInjector() {
417
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
418
  }
419
420
  private void setVariableNameInjector( final VariableNameInjector injector ) {
421
    this.variableNameInjector = injector;
422
  }
423
424
  private synchronized VariableNameInjector getVariableNameInjector() {
425
    if( this.variableNameInjector == null ) {
426
      final VariableNameInjector vin = createVariableNameInjector();
427
      setVariableNameInjector( vin );
428
    }
429
430
    return this.variableNameInjector;
431
  }
432
433
  private VariableNameInjector createVariableNameInjector() {
434
    final FileEditorTab tab = getActiveFileEditor();
435
    final DefinitionPane pane = getDefinitionPane();
436
437
    return new VariableNameInjector( tab, pane );
438
  }
439
440
  /**
441
   * Called whenever the preview pane becomes out of sync with the file editor
442
   * tab. This can be called when the text changes, the caret paragraph changes,
443
   * or the file tab changes.
444
   *
445
   * @param tab The file editor tab that has been changed in some fashion.
446
   */
447
  private void refreshSelectedTab( final FileEditorTab tab ) {
448
    if( tab == null ) {
449
      return;
450
    }
451
452
    getPreviewPane().setPath( tab.getPath() );
453
454
    Processor<String> processor = getProcessors().get( tab );
455
456
    if( processor == null ) {
457
      processor = createProcessor( tab );
458
      getProcessors().put( tab, processor );
459
    }
460
461
    try {
462
      processor.processChain( tab.getEditorText() );
463
    } catch( final Exception ex ) {
464
      error( ex );
465
    }
466
  }
467
468
  private void refreshActiveTab() {
469
    refreshSelectedTab( getActiveFileEditor() );
470
  }
471
472
  /**
473
   * Called when a definition source is opened.
474
   *
475
   * @param path Path to the definition source that was opened.
476
   */
477
  private void openDefinitions( final Path path ) {
478
    try {
479
      final DefinitionSource ds = createDefinitionSource( path );
480
      setDefinitionSource( ds );
481
      getUserPreferences().definitionPathProperty().setValue( path.toFile() );
482
      getUserPreferences().save();
483
484
      final Tooltip tooltipPath = new Tooltip( path.toString() );
485
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
486
487
      final DefinitionPane pane = getDefinitionPane();
488
      pane.update( ds );
489
      pane.addTreeChangeHandler( mTreeHandler );
490
      pane.addKeyEventHandler( mDefinitionKeyHandler );
491
      pane.filenameProperty().setValue( path.getFileName().toString() );
492
      pane.setTooltip( tooltipPath );
493
494
      interpolateResolvedMap();
495
    } catch( final Exception e ) {
496
      error( e );
497
    }
498
  }
499
500
  private void exportDefinitions( final Path path ) {
501
    try {
502
      final DefinitionPane pane = getDefinitionPane();
503
      final TreeItem<String> root = pane.getTreeView().getRoot();
504
      final TreeItem<String> problemChild = pane.isTreeWellFormed();
505
506
      if( problemChild == null ) {
507
        getDefinitionSource().getTreeAdapter().export( root, path );
508
        getNotifier().clear();
509
      }
510
      else {
511
        final String msg = get( "yaml.error.tree.form",
512
                                problemChild.getValue() );
513
        getNotifier().notify( msg );
514
      }
515
    } catch( final Exception e ) {
516
      error( e );
517
    }
518
  }
519
520
  private void interpolateResolvedMap() {
521
    final Map<String, String> treeMap = getDefinitionPane().toMap();
522
    final Map<String, String> map = new HashMap<>( treeMap );
523
    MapInterpolator.interpolate( map );
524
525
    getResolvedMap().clear();
526
    getResolvedMap().putAll( map );
527
  }
528
529
  private void restoreDefinitionPane() {
530
    openDefinitions( getDefinitionPath() );
531
  }
532
533
  /**
534
   * Called when the last open tab is closed to clear the preview pane.
535
   */
536
  private void closeRemainingTab() {
537
    getPreviewPane().clear();
538
  }
539
540
  /**
541
   * Called when an exception occurs that warrants the user's attention.
542
   *
543
   * @param e The exception with a message that the user should know about.
544
   */
545
  private void error( final Exception e ) {
546
    getNotifier().notify( e );
547
  }
548
549
  //---- File actions -------------------------------------------------------
550
551
  /**
552
   * Called when an observable instance has changed. This is called by both the
553
   * snitch service and the notify service. The snitch service can be called for
554
   * different file types, including definition sources.
555
   *
556
   * @param observable The observed instance.
557
   * @param value      The noteworthy item.
558
   */
559
  @Override
560
  public void update( final Observable observable, final Object value ) {
561
    if( value != null ) {
562
      if( observable instanceof Snitch && value instanceof Path ) {
563
        updateSelectedTab();
564
      }
565
      else if( observable instanceof Notifier && value instanceof String ) {
566
        updateStatusBar( (String) value );
567
      }
568
    }
569
  }
570
571
  /**
572
   * Updates the status bar to show the given message.
573
   *
574
   * @param s The message to show in the status bar.
575
   */
576
  private void updateStatusBar( final String s ) {
577
    Platform.runLater(
578
        () -> {
579
          final int index = s.indexOf( '\n' );
580
          final String message = s.substring(
581
              0, index > 0 ? index : s.length() );
582
583
          getStatusBar().setText( message );
584
        }
585
    );
586
  }
587
588
  /**
589
   * Called when a file has been modified.
590
   */
591
  private void updateSelectedTab() {
592
    Platform.runLater(
593
        () -> {
594
          // Brute-force XSLT file reload by re-instantiating all processors.
595
          resetProcessors();
596
          refreshActiveTab();
597
        }
598
    );
599
  }
600
601
  /**
602
   * After resetting the processors, they will refresh anew to be up-to-date
603
   * with the files (text and definition) currently loaded into the editor.
604
   */
605
  private void resetProcessors() {
606
    getProcessors().clear();
607
  }
608
609
  //---- File actions -------------------------------------------------------
610
611
  private void fileNew() {
612
    getFileEditorPane().newEditor();
613
  }
614
615
  private void fileOpen() {
616
    getFileEditorPane().openFileDialog();
617
  }
618
619
  private void fileClose() {
620
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
621
  }
622
623
  /**
624
   * TODO: Upon closing, first remove the tab change listeners. (There's no
625
   * need to re-render each tab when all are being closed.)
626
   */
627
  private void fileCloseAll() {
628
    getFileEditorPane().closeAllEditors();
629
  }
630
631
  private void fileSave() {
632
    getFileEditorPane().saveEditor( getActiveFileEditor() );
633
  }
634
635
  private void fileSaveAs() {
636
    final FileEditorTab editor = getActiveFileEditor();
637
    getFileEditorPane().saveEditorAs( editor );
638
    getProcessors().remove( editor );
639
640
    try {
641
      refreshSelectedTab( editor );
642
    } catch( final Exception ex ) {
643
      getNotifier().notify( ex );
644
    }
645
  }
646
647
  private void fileSaveAll() {
648
    getFileEditorPane().saveAllEditors();
649
  }
650
651
  private void fileExit() {
652
    final Window window = getWindow();
653
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
654
  }
655
656
  //---- Edit actions -------------------------------------------------------
657
658
  /**
659
   * Used to find text in the active file editor window.
660
   */
661
  private void editFind() {
662
    final TextField input = getFindTextField();
663
    getStatusBar().setGraphic( input );
664
    input.requestFocus();
665
  }
666
667
  public void editFindNext() {
668
    getActiveFileEditor().searchNext( getFindTextField().getText() );
669
  }
670
671
  public void editPreferences() {
672
    getUserPreferences().show();
673
  }
674
675
  //---- Insert actions -----------------------------------------------------
676
677
  /**
678
   * Delegates to the active editor to handle wrapping the current text
679
   * selection with leading and trailing strings.
680
   *
681
   * @param leading  The string to put before the selection.
682
   * @param trailing The string to put after the selection.
683
   */
684
  private void insertMarkdown(
685
      final String leading, final String trailing ) {
686
    getActiveEditor().surroundSelection( leading, trailing );
687
  }
688
689
  @SuppressWarnings("SameParameterValue")
690
  private void insertMarkdown(
691
      final String leading, final String trailing, final String hint ) {
692
    getActiveEditor().surroundSelection( leading, trailing, hint );
693
  }
694
695
  //---- Help actions -------------------------------------------------------
696
697
  private void helpAbout() {
698
    final Alert alert = new Alert( AlertType.INFORMATION );
699
    alert.setTitle( get( "Dialog.about.title" ) );
700
    alert.setHeaderText( get( "Dialog.about.header" ) );
701
    alert.setContentText( get( "Dialog.about.content" ) );
702
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
703
    alert.initOwner( getWindow() );
704
705
    alert.showAndWait();
706
  }
707
708
  //---- Member creators ----------------------------------------------------
709
710
  /**
711
   * Factory to create processors that are suited to different file types.
712
   *
713
   * @param tab The tab that is subjected to processing.
714
   * @return A processor suited to the file type specified by the tab's path.
715
   */
716
  private Processor<String> createProcessor( final FileEditorTab tab ) {
717
    return createProcessorFactory().createProcessor( tab );
718
  }
719
720
  private ProcessorFactory createProcessorFactory() {
721
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
722
  }
723
724
  private HTMLPreviewPane createHTMLPreviewPane() {
725
    return new HTMLPreviewPane();
726
  }
727
728
  private DefinitionSource createDefaultDefinitionSource() {
729
    return new YamlDefinitionSource( getDefinitionPath() );
730
  }
731
732
  private DefinitionSource createDefinitionSource( final Path path ) {
733
    try {
734
      return createDefinitionFactory().createDefinitionSource( path );
735
    } catch( final Exception ex ) {
736
      error( ex );
737
      return createDefaultDefinitionSource();
738
    }
739
  }
740
741
  private TextField createFindTextField() {
742
    return new TextField();
743
  }
744
745
  /**
746
   * Create an editor pane to hold file editor tabs.
747
   *
748
   * @return A new instance, never null.
749
   */
750
  private FileEditorTabPane createFileEditorPane() {
751
    return new FileEditorTabPane( mScrollEventObserver, mCaretListener );
752
  }
753
754
  private DefinitionFactory createDefinitionFactory() {
755
    return new DefinitionFactory();
756
  }
757
758
  private StatusBar createStatusBar() {
759
    return new StatusBar();
760
  }
761
762
  private Scene createScene() {
763
    final SplitPane splitPane = new SplitPane(
764
        getDefinitionPane().getNode(),
765
        getFileEditorPane().getNode(),
766
        getPreviewPane().getNode() );
767
768
    splitPane.setDividerPositions(
769
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
770
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
771
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
772
773
    getDefinitionPane().prefHeightProperty().bind( splitPane.heightProperty() );
774
775
    final BorderPane borderPane = new BorderPane();
776
    borderPane.setPrefSize( 1024, 800 );
777
    borderPane.setTop( createMenuBar() );
778
    borderPane.setBottom( getStatusBar() );
779
    borderPane.setCenter( splitPane );
780
781
    final VBox statusBar = new VBox();
782
    statusBar.setAlignment( Pos.BASELINE_CENTER );
783
    statusBar.getChildren().add( getLineNumberText() );
784
    getStatusBar().getRightItems().add( statusBar );
785
786
    return new Scene( borderPane );
787
  }
788
789
  private Text createLineNumberText() {
790
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
791
  }
792
793
  private Node createMenuBar() {
794
    final BooleanBinding activeFileEditorIsNull =
795
        getFileEditorPane().activeFileEditorProperty().isNull();
796
797
    // File actions
798
    final Action fileNewAction = new ActionBuilder()
799
        .setText( "Main.menu.file.new" )
800
        .setAccelerator( "Shortcut+N" )
801
        .setIcon( FILE_ALT )
802
        .setAction( e -> fileNew() )
803
        .build();
804
    final Action fileOpenAction = new ActionBuilder()
805
        .setText( "Main.menu.file.open" )
806
        .setAccelerator( "Shortcut+O" )
807
        .setIcon( FOLDER_OPEN_ALT )
808
        .setAction( e -> fileOpen() )
809
        .build();
810
    final Action fileCloseAction = new ActionBuilder()
811
        .setText( "Main.menu.file.close" )
812
        .setAccelerator( "Shortcut+W" )
813
        .setAction( e -> fileClose() )
814
        .setDisable( activeFileEditorIsNull )
815
        .build();
816
    final Action fileCloseAllAction = new ActionBuilder()
817
        .setText( "Main.menu.file.close_all" )
818
        .setAction( e -> fileCloseAll() )
819
        .setDisable( activeFileEditorIsNull )
820
        .build();
821
    final Action fileSaveAction = new ActionBuilder()
822
        .setText( "Main.menu.file.save" )
823
        .setAccelerator( "Shortcut+S" )
824
        .setIcon( FLOPPY_ALT )
825
        .setAction( e -> fileSave() )
826
        .setDisable( createActiveBooleanProperty(
827
            FileEditorTab::modifiedProperty ).not() )
828
        .build();
829
    final Action fileSaveAsAction = new ActionBuilder()
830
        .setText( "Main.menu.file.save_as" )
831
        .setAction( e -> fileSaveAs() )
832
        .setDisable( activeFileEditorIsNull )
833
        .build();
834
    final Action fileSaveAllAction = new ActionBuilder()
835
        .setText( "Main.menu.file.save_all" )
836
        .setAccelerator( "Shortcut+Shift+S" )
837
        .setAction( e -> fileSaveAll() )
838
        .setDisable( Bindings.not(
839
            getFileEditorPane().anyFileEditorModifiedProperty() ) )
840
        .build();
841
    final Action fileExitAction = new ActionBuilder()
842
        .setText( "Main.menu.file.exit" )
843
        .setAction( e -> fileExit() )
844
        .build();
845
846
    // Edit actions
847
    final Action editUndoAction = new ActionBuilder()
848
        .setText( "Main.menu.edit.undo" )
849
        .setAccelerator( "Shortcut+Z" )
850
        .setIcon( UNDO )
851
        .setAction( e -> getActiveEditor().undo() )
852
        .setDisable( createActiveBooleanProperty(
853
            FileEditorTab::canUndoProperty ).not() )
854
        .build();
855
    final Action editRedoAction = new ActionBuilder()
856
        .setText( "Main.menu.edit.redo" )
857
        .setAccelerator( "Shortcut+Y" )
858
        .setIcon( REPEAT )
859
        .setAction( e -> getActiveEditor().redo() )
860
        .setDisable( createActiveBooleanProperty(
861
            FileEditorTab::canRedoProperty ).not() )
862
        .build();
863
    final Action editFindAction = new ActionBuilder()
864
        .setText( "Main.menu.edit.find" )
865
        .setAccelerator( "Ctrl+F" )
866
        .setIcon( SEARCH )
867
        .setAction( e -> editFind() )
868
        .setDisable( activeFileEditorIsNull )
869
        .build();
870
    final Action editFindNextAction = new ActionBuilder()
871
        .setText( "Main.menu.edit.find.next" )
872
        .setAccelerator( "F3" )
873
        .setIcon( null )
874
        .setAction( e -> editFindNext() )
875
        .setDisable( activeFileEditorIsNull )
876
        .build();
877
    final Action editPreferencesAction = new ActionBuilder()
878
        .setText( "Main.menu.edit.preferences" )
879
        .setAccelerator( "Ctrl+Alt+S" )
880
        .setAction( e -> editPreferences() )
881
        .build();
882
883
    // Insert actions
884
    final Action insertBoldAction = new ActionBuilder()
885
        .setText( "Main.menu.insert.bold" )
886
        .setAccelerator( "Shortcut+B" )
887
        .setIcon( BOLD )
888
        .setAction( e -> insertMarkdown( "**", "**" ) )
889
        .setDisable( activeFileEditorIsNull )
890
        .build();
891
    final Action insertItalicAction = new ActionBuilder()
892
        .setText( "Main.menu.insert.italic" )
893
        .setAccelerator( "Shortcut+I" )
894
        .setIcon( ITALIC )
895
        .setAction( e -> insertMarkdown( "*", "*" ) )
896
        .setDisable( activeFileEditorIsNull )
897
        .build();
898
    final Action insertSuperscriptAction = new ActionBuilder()
899
        .setText( "Main.menu.insert.superscript" )
900
        .setAccelerator( "Shortcut+[" )
901
        .setIcon( SUPERSCRIPT )
902
        .setAction( e -> insertMarkdown( "^", "^" ) )
903
        .setDisable( activeFileEditorIsNull )
904
        .build();
905
    final Action insertSubscriptAction = new ActionBuilder()
906
        .setText( "Main.menu.insert.subscript" )
907
        .setAccelerator( "Shortcut+]" )
908
        .setIcon( SUBSCRIPT )
909
        .setAction( e -> insertMarkdown( "~", "~" ) )
910
        .setDisable( activeFileEditorIsNull )
911
        .build();
912
    final Action insertStrikethroughAction = new ActionBuilder()
913
        .setText( "Main.menu.insert.strikethrough" )
914
        .setAccelerator( "Shortcut+T" )
915
        .setIcon( STRIKETHROUGH )
916
        .setAction( e -> insertMarkdown( "~~", "~~" ) )
917
        .setDisable( activeFileEditorIsNull )
918
        .build();
919
    final Action insertBlockquoteAction = new ActionBuilder()
920
        .setText( "Main.menu.insert.blockquote" )
921
        .setAccelerator( "Ctrl+Q" )
922
        .setIcon( QUOTE_LEFT )
923
        .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
924
        .setDisable( activeFileEditorIsNull )
925
        .build();
926
    final Action insertCodeAction = new ActionBuilder()
927
        .setText( "Main.menu.insert.code" )
928
        .setAccelerator( "Shortcut+K" )
929
        .setIcon( CODE )
930
        .setAction( e -> insertMarkdown( "`", "`" ) )
931
        .setDisable( activeFileEditorIsNull )
932
        .build();
933
    final Action insertFencedCodeBlockAction = new ActionBuilder()
934
        .setText( "Main.menu.insert.fenced_code_block" )
935
        .setAccelerator( "Shortcut+Shift+K" )
936
        .setIcon( FILE_CODE_ALT )
937
        .setAction( e -> getActiveEditor().surroundSelection(
938
            "\n\n```\n",
939
            "\n```\n\n",
940
            get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
941
        .setDisable( activeFileEditorIsNull )
942
        .build();
943
    final Action insertLinkAction = new ActionBuilder()
944
        .setText( "Main.menu.insert.link" )
945
        .setAccelerator( "Shortcut+L" )
946
        .setIcon( LINK )
947
        .setAction( e -> getActiveEditor().insertLink() )
948
        .setDisable( activeFileEditorIsNull )
949
        .build();
950
    final Action insertImageAction = new ActionBuilder()
951
        .setText( "Main.menu.insert.image" )
952
        .setAccelerator( "Shortcut+G" )
953
        .setIcon( PICTURE_ALT )
954
        .setAction( e -> getActiveEditor().insertImage() )
955
        .setDisable( activeFileEditorIsNull )
956
        .build();
957
958
    // Number of header actions (H1 ... H3)
959
    final int HEADERS = 3;
960
    final Action[] headers = new Action[ HEADERS ];
961
962
    for( int i = 1; i <= HEADERS; i++ ) {
963
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
964
      final String markup = String.format( "%n%n%s ", hashes );
965
      final String text = "Main.menu.insert.header." + i;
966
      final String accelerator = "Shortcut+" + i;
967
      final String prompt = text + ".prompt";
968
969
      headers[ i - 1 ] = new ActionBuilder()
970
          .setText( text )
971
          .setAccelerator( accelerator )
972
          .setIcon( HEADER )
973
          .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
974
          .setDisable( activeFileEditorIsNull )
975
          .build();
976
    }
977
978
    final Action insertUnorderedListAction = new ActionBuilder()
979
        .setText( "Main.menu.insert.unordered_list" )
980
        .setAccelerator( "Shortcut+U" )
981
        .setIcon( LIST_UL )
982
        .setAction( e -> getActiveEditor()
983
            .surroundSelection( "\n\n* ", "" ) )
984
        .setDisable( activeFileEditorIsNull )
985
        .build();
986
    final Action insertOrderedListAction = new ActionBuilder()
987
        .setText( "Main.menu.insert.ordered_list" )
988
        .setAccelerator( "Shortcut+Shift+O" )
989
        .setIcon( LIST_OL )
990
        .setAction( e -> insertMarkdown(
991
            "\n\n1. ", "" ) )
992
        .setDisable( activeFileEditorIsNull )
993
        .build();
994
    final Action insertHorizontalRuleAction = new ActionBuilder()
995
        .setText( "Main.menu.insert.horizontal_rule" )
996
        .setAccelerator( "Shortcut+H" )
997
        .setAction( e -> insertMarkdown(
998
            "\n\n---\n\n", "" ) )
999
        .setDisable( activeFileEditorIsNull )
1000
        .build();
1001
1002
    // Help actions
1003
    final Action helpAboutAction = new ActionBuilder()
1004
        .setText( "Main.menu.help.about" )
1005
        .setAction( e -> helpAbout() )
1006
        .build();
1007
1008
    //---- MenuBar ----
1009
    final Menu fileMenu = ActionUtils.createMenu(
1010
        get( "Main.menu.file" ),
1011
        fileNewAction,
1012
        fileOpenAction,
1013
        null,
1014
        fileCloseAction,
1015
        fileCloseAllAction,
1016
        null,
1017
        fileSaveAction,
1018
        fileSaveAsAction,
1019
        fileSaveAllAction,
1020
        null,
1021
        fileExitAction );
1022
1023
    final Menu editMenu = ActionUtils.createMenu(
1024
        get( "Main.menu.edit" ),
1025
        editUndoAction,
1026
        editRedoAction,
1027
        editFindAction,
1028
        editFindNextAction,
1029
        null,
1030
        editPreferencesAction );
1031
1032
    final Menu insertMenu = ActionUtils.createMenu(
1033
        get( "Main.menu.insert" ),
1034
        insertBoldAction,
1035
        insertItalicAction,
1036
        insertSuperscriptAction,
1037
        insertSubscriptAction,
1038
        insertStrikethroughAction,
1039
        insertBlockquoteAction,
1040
        insertCodeAction,
1041
        insertFencedCodeBlockAction,
1042
        null,
1043
        insertLinkAction,
1044
        insertImageAction,
1045
        null,
1046
        headers[ 0 ],
1047
        headers[ 1 ],
1048
        headers[ 2 ],
1049
        null,
1050
        insertUnorderedListAction,
1051
        insertOrderedListAction,
1052
        insertHorizontalRuleAction );
1053
1054
    final Menu helpMenu = ActionUtils.createMenu(
1055
        get( "Main.menu.help" ),
1056
        helpAboutAction );
1057
1058
    final MenuBar menuBar = new MenuBar(
1059
        fileMenu,
1060
        editMenu,
1061
        insertMenu,
1062
        helpMenu );
1063
1064
    //---- ToolBar ----
1065
    final ToolBar toolBar = ActionUtils.createToolBar(
1066
        fileNewAction,
1067
        fileOpenAction,
1068
        fileSaveAction,
1069
        null,
1070
        editUndoAction,
1071
        editRedoAction,
1072
        null,
1073
        insertBoldAction,
1074
        insertItalicAction,
1075
        insertSuperscriptAction,
1076
        insertSubscriptAction,
1077
        insertBlockquoteAction,
1078
        insertCodeAction,
1079
        insertFencedCodeBlockAction,
1080
        null,
1081
        insertLinkAction,
1082
        insertImageAction,
1083
        null,
1084
        headers[ 0 ],
1085
        null,
1086
        insertUnorderedListAction,
1087
        insertOrderedListAction );
1088
1089
    return new VBox( menuBar, toolBar );
1090
  }
1091
1092
  /**
1093
   * Creates a boolean property that is bound to another boolean value of the
1094
   * active editor.
1095
   */
1096
  private BooleanProperty createActiveBooleanProperty(
1097
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1098
1099
    final BooleanProperty b = new SimpleBooleanProperty();
1100
    final FileEditorTab tab = getActiveFileEditor();
1101
1102
    if( tab != null ) {
1103
      b.bind( func.apply( tab ) );
1104
    }
1105
1106
    getFileEditorPane().activeFileEditorProperty().addListener(
1107
        ( observable, oldFileEditor, newFileEditor ) -> {
1108
          b.unbind();
1109
1110
          if( newFileEditor == null ) {
1111
            b.set( false );
1112
          }
1113
          else {
1114
            b.bind( func.apply( newFileEditor ) );
1115
          }
1116
        }
1117
    );
1118
1119
    return b;
1120
  }
1121
1122
  //---- Convenience accessors ----------------------------------------------
1123
1124
  private Preferences getPreferences() {
1125
    return OPTIONS.getState();
1126
  }
1127
1128
  private float getFloat( final String key, final float defaultValue ) {
1129
    return getPreferences().getFloat( key, defaultValue );
1130
  }
1131
1132
  public Window getWindow() {
1133
    return getScene().getWindow();
1134
  }
1135
1136
  private MarkdownEditorPane getActiveEditor() {
1137
    final EditorPane pane = getActiveFileEditor().getEditorPane();
1138
1139
    return pane instanceof MarkdownEditorPane
1140
        ? (MarkdownEditorPane) pane
1141
        : new MarkdownEditorPane();
1142
  }
1143
1144
  private FileEditorTab getActiveFileEditor() {
1145
    return getFileEditorPane().getActiveFileEditor();
1146
  }
1147
1148
  //---- Member accessors ---------------------------------------------------
1149
1150
  protected Scene getScene() {
1151
    return mScene;
1152
  }
1153
1154
  private Map<FileEditorTab, Processor<String>> getProcessors() {
1155
    return mProcessors;
1156
  }
1157
1158
  private FileEditorTabPane getFileEditorPane() {
1159
    var pane = mFileEditorPane;
1160
1161
    if( pane == null ) {
1162
      pane = createFileEditorPane();
1163
    }
1164
1165
    return mFileEditorPane = pane;
1166
  }
1167
1168
  private HTMLPreviewPane getPreviewPane() {
1169
    return mPreviewPane;
1170
  }
1171
1172
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
1173
    assert definitionSource != null;
1174
    mDefinitionSource = definitionSource;
1175
  }
1176
1177
  private DefinitionSource getDefinitionSource() {
1178
    return mDefinitionSource;
1179
  }
1180
1181
  private DefinitionPane getDefinitionPane() {
1182
    return mDefinitionPane;
1183
  }
1184
1185
  private Text getLineNumberText() {
1186
    return mLineNumberText;
1187
  }
1188
1189
  private StatusBar getStatusBar() {
1190
    return mStatusBar;
1191
  }
1192
1193
  private TextField getFindTextField() {
1194
    return mFindTextField;
1195
  }
1196
1197
  /**
1198
   * Returns the variable map of interpolated definitions.
1199
   *
1200
   * @return A map to help dereference variables.
1201
   */
1202
  private Map<String, String> getResolvedMap() {
1203
    return mResolvedMap;
1204
  }
1205
1206
  private Notifier getNotifier() {
1207
    return NOTIFIER;
1208
  }
1209
1210
  //---- Persistence accessors ----------------------------------------------
1211
  private UserPreferences getUserPreferences() {
1212
    return OPTIONS.getUserPreferences();
1213
  }
1214
1215
  private Path getDefinitionPath() {
1216
    return getUserPreferences().getDefinitionPath();
12111217
  }
12121218
}
M src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
3131
import com.scrivenvar.dialogs.LinkDialog;
3232
import com.scrivenvar.editors.EditorPane;
33
import com.scrivenvar.processors.markdown.BlockExtension;
3334
import com.scrivenvar.processors.markdown.MarkdownProcessor;
3435
import com.vladsch.flexmark.ast.Link;
36
import com.vladsch.flexmark.html.renderer.AttributablePart;
3537
import com.vladsch.flexmark.util.ast.Node;
38
import com.vladsch.flexmark.util.html.MutableAttributes;
39
import javafx.beans.value.ChangeListener;
3640
import javafx.scene.control.Dialog;
3741
import javafx.scene.control.IndexRange;
...
5660
 */
5761
public class MarkdownEditorPane extends EditorPane {
58
5962
  private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile(
6063
      "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
...
7477
  }
7578
76
  private void enterPressed( final KeyEvent e ) {
77
    final StyleClassedTextArea textArea = getEditor();
78
    final String currentLine =
79
        textArea.getText( textArea.getCurrentParagraph() );
80
    final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
79
  public void insertLink() {
80
    insertObject( createLinkDialog() );
81
  }
8182
82
    String newText = "\n";
83
  public void insertImage() {
84
    insertObject( createImageDialog() );
85
  }
8386
84
    if( matcher.matches() ) {
85
      if( !matcher.group( 2 ).isEmpty() ) {
86
        // indent new line with same whitespace characters and list markers
87
        // as current line
88
        newText = newText.concat( matcher.group( 1 ) );
89
      }
90
      else {
91
        // current line contains only whitespace characters and list markers
92
        // --> empty current line
93
        final int caretPosition = textArea.getCaretPosition();
94
        textArea.selectRange( caretPosition - currentLine.length(),
95
                              caretPosition );
96
      }
97
    }
87
  public void addCaretListener( final ChangeListener<Integer> listener ) {
88
    getEditor().caretPositionProperty().addListener( listener );
89
  }
9890
99
    textArea.replaceSelection( newText );
91
  /**
92
   * Aligns the editor's paragraph number with the paragraph number generated
93
   * by HTML. Ultimately this solution is flawed because there isn't
94
   * a straightforward correlation between the document being edited and
95
   * what is rendered. XML documents transformed through stylesheets have
96
   * no readily determined correlation. Images, tables, and other
97
   * objects affect the relative location of the current paragraph being
98
   * edited with respect to the preview pane.
99
   * <p>
100
   * See
101
   * {@link BlockExtension.IdAttributeProvider#setAttributes(Node, AttributablePart, MutableAttributes)}}
102
   * for details.
103
   * </p>
104
   * <p>
105
   * Injecting a token into the document, as per a previous version of the
106
   * application, can instruct the preview pane where to shift the viewport.
107
   * </p>
108
   *
109
   * @return A unique identifier that correlates to the preview pane.
110
   */
111
  public String getCurrentParagraphId() {
112
    final StyleClassedTextArea editor = getEditor();
113
    final int paraIndex = editor.getCurrentParagraph();
114
    int i = 0, paragraphs = 0;
100115
101
    // Ensure that the window scrolls when Enter is pressed at the bottom of
102
    // the pane.
103
    textArea.requestFollowCaret();
116
    while( i < paraIndex ) {
117
      final String text = editor.getParagraph( i++ ).getText().trim();
118
119
      paragraphs += text.isEmpty() || text.equals( ">" ) ? 0 : 1;
120
    }
121
122
    return "para-" + paragraphs;
104123
  }
105124
106125
  public void surroundSelection( final String leading, final String trailing ) {
107126
    surroundSelection( leading, trailing, null );
108127
  }
109128
110
  public void surroundSelection( String leading, String trailing,
111
                                 final String hint ) {
129
  public void surroundSelection(
130
      String leading, String trailing, final String hint ) {
112131
    final StyleClassedTextArea textArea = getEditor();
113132
...
120139
    final String selectedText = textArea.getSelectedText();
121140
122
    // remove leading and trailing whitespaces from selected text
123
    String trimmedSelectedText = selectedText.trim();
124
    if( trimmedSelectedText.length() < selectedText.length() ) {
125
      start += selectedText.indexOf( trimmedSelectedText );
126
      end = start + trimmedSelectedText.length();
141
    String trimmedText = selectedText.trim();
142
    if( trimmedText.length() < selectedText.length() ) {
143
      start += selectedText.indexOf( trimmedText );
144
      end = start + trimmedText.length();
127145
    }
128146
...
145163
          break;
146164
        }
165
147166
        leading = leading.substring( 1 );
148167
      }
...
177196
178197
    // insert hint text if selection is empty
179
    if( hint != null && trimmedSelectedText.isEmpty() ) {
180
      trimmedSelectedText = hint;
198
    if( hint != null && trimmedText.isEmpty() ) {
199
      trimmedText = hint;
181200
      selEnd = selStart + hint.length();
182201
    }
183202
184203
    // prevent undo merging with previous text entered by user
185204
    getUndoManager().preventMerge();
186205
187206
    // replace text and update selection
188
    textArea.replaceText( start,
189
                          end,
190
                          leading + trimmedSelectedText + trailing );
207
    textArea.replaceText( start, end, leading + trimmedText + trailing );
191208
    textArea.selectRange( selStart, selEnd );
209
  }
210
211
  private void enterPressed( final KeyEvent e ) {
212
    final StyleClassedTextArea textArea = getEditor();
213
    final String currentLine =
214
        textArea.getText( textArea.getCurrentParagraph() );
215
    final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
216
217
    String newText = "\n";
218
219
    if( matcher.matches() ) {
220
      if( !matcher.group( 2 ).isEmpty() ) {
221
        // indent new line with same whitespace characters and list markers
222
        // as current line
223
        newText = newText.concat( matcher.group( 1 ) );
224
      }
225
      else {
226
        // current line contains only whitespace characters and list markers
227
        // --> empty current line
228
        final int caretPosition = textArea.getCaretPosition();
229
        textArea.selectRange( caretPosition - currentLine.length(),
230
                              caretPosition );
231
      }
232
    }
233
234
    textArea.replaceSelection( newText );
235
236
    // Ensure that the window scrolls when Enter is pressed at the bottom of
237
    // the pane.
238
    textArea.requestFollowCaret();
192239
  }
193240
...
245292
        result -> getEditor().replaceSelection( result )
246293
    );
247
  }
248
249
  public void insertLink() {
250
    insertObject( createLinkDialog() );
251
  }
252
253
  public void insertImage() {
254
    insertObject( createImageDialog() );
255294
  }
256295
M src/main/java/com/scrivenvar/preview/ChainedReplacedElementFactory.java
3939
4040
  public ReplacedElement createReplacedElement(
41
      LayoutContext c, BlockBox box, UserAgentCallback uac,
42
      int cssWidth, int cssHeight ) {
43
    ReplacedElement re = null;
44
45
    for( final ReplacedElementFactory ref : mFactoryList ) {
46
      re = ref.createReplacedElement( c, box, uac, cssWidth, cssHeight );
41
      final LayoutContext c,
42
      final BlockBox box,
43
      final UserAgentCallback uac,
44
      final int cssWidth,
45
      final int cssHeight ) {
46
    for( final var f : mFactoryList ) {
47
      final var r = f.createReplacedElement( c, box, uac, cssWidth, cssHeight );
4748
48
      if( re != null ) {
49
        break;
49
      if( r != null ) {
50
        return r;
5051
      }
5152
    }
5253
53
    return re;
54
    return null;
5455
  }
5556
5657
  public void addFactory( final ReplacedElementFactory factory ) {
5758
    mFactoryList.add( factory );
5859
  }
5960
6061
  public void reset() {
61
    for( final ReplacedElementFactory factory : mFactoryList ) {
62
    for( final var factory : mFactoryList ) {
6263
      factory.reset();
6364
    }
6465
  }
6566
6667
  public void remove( final Element element ) {
67
    for( final ReplacedElementFactory factory : mFactoryList ) {
68
    for( final var factory : mFactoryList ) {
6869
      factory.remove( element );
6970
    }
M src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
3434
import org.jsoup.helper.W3CDom;
3535
import org.jsoup.nodes.Document;
36
import org.xhtmlrenderer.layout.SharedContext;
37
import org.xhtmlrenderer.render.Box;
3638
import org.xhtmlrenderer.simple.XHTMLPanel;
3739
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
3840
import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
3941
4042
import javax.swing.*;
43
import java.awt.*;
4144
import java.nio.file.Path;
4245
...
4952
 */
5053
public final class HTMLPreviewPane extends Pane {
54
  /**
55
   * Prevent scrolling to the top on every key press.
56
   */
5157
  private static class HTMLPanel extends XHTMLPanel {
52
    /**
53
     * Prevent scrolling to the top.
54
     */
5558
    @Override
5659
    public void resetScrollPosition() {
...
8487
   */
8588
  public HTMLPreviewPane() {
86
    final ChainedReplacedElementFactory factory =
87
        new ChainedReplacedElementFactory();
89
    final var factory = new ChainedReplacedElementFactory();
8890
    factory.addFactory( new SVGReplacedElementFactory() );
8991
    factory.addFactory( new SwingReplacedElementFactory() );
9092
91
    mRenderer.getSharedContext().setReplacedElementFactory( factory );
92
    mRenderer.getSharedContext().getTextRenderer().setSmoothingThreshold( 0 );
93
    final var context = getSharedContext();
94
    context.setReplacedElementFactory( factory );
95
    context.getTextRenderer().setSmoothingThreshold( 0 );
96
9397
    mSwingNode.setContent( mScrollPane );
9498
...
105109
  public void update( final String html ) {
106110
    final Document jsoupDoc = Jsoup.parse( decorate( html ) );
107
    org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc );
111
    final org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc );
108112
109113
    mRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler );
114
  }
115
116
  /**
117
   * Scrolls to an anchor link. The anchor links are injected when the
118
   * HTML document is created.
119
   *
120
   * @param id The unique anchor link identifier.
121
   */
122
  public void scrollTo( final String id ) {
123
    assert id != null;
124
    final Box box = getSharedContext().getBoxById( id );
125
126
    if( box != null ) {
127
      mRenderer.scrollTo( createPoint( box ) );
128
    }
110129
  }
111130
112131
  private String decorate( final String html ) {
132
    // Trim the HTML back to the header.
113133
    mHtml.setLength( mHtmlPrefixLength );
134
135
    // Write the HTML body element followed by closing tags.
114136
    return mHtml.append( html )
115137
                .append( HTML_FOOTER )
...
122144
  public void clear() {
123145
    update( "" );
124
  }
125
126
  private String getBaseUrl() {
127
    final Path basePath = getPath();
128
    final Path parent = basePath == null ? null : basePath.getParent();
129
130
    return parent == null ? "" : parent.toUri().toString();
131146
  }
132147
...
155170
  public JScrollBar getVerticalScrollBar() {
156171
    return getScrollPane().getVerticalScrollBar();
172
  }
173
174
  private Point createPoint( final Box box ) {
175
    assert box != null;
176
177
    int x = box.getAbsX();
178
179
    // Scroll back up by half the height of the scroll bar to keep the typing
180
    // area within the view port. Otherwise the view port will have jumped too
181
    // high up and the whatever gets typed won't be visible.
182
    int y = Math.abs(
183
        box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2)
184
    );
185
186
    if( !box.getStyle().isInline() ) {
187
      final var margin = box.getMargin( mRenderer.getLayoutContext() );
188
      x += margin.left();
189
      y += margin.top();
190
    }
191
192
    return new Point( x, y );
193
  }
194
195
  private String getBaseUrl() {
196
    final Path basePath = getPath();
197
    final Path parent = basePath == null ? null : basePath.getParent();
198
199
    return parent == null ? "" : parent.toUri().toString();
200
  }
201
202
  private SharedContext getSharedContext() {
203
    return mRenderer.getSharedContext();
157204
  }
158205
}
M src/main/java/com/scrivenvar/preview/SVGRasterizer.java
4242
import java.util.Map;
4343
44
import static java.awt.Color.RED;
4445
import static java.awt.Color.WHITE;
4546
import static java.awt.RenderingHints.*;
47
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
4648
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
4749
import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_BACKGROUND_COLOR;
...
8789
    }
8890
89
    public BufferedImage getBufferedImage() {
91
    public Image getImage() {
9092
      return mImage;
9193
    }
...
103105
  }
104106
105
  public static BufferedImage rasterize( final String url, final int width )
106
      throws IOException, TranscoderException {
107
    return rasterize( new URL( url ), width );
107
  /**
108
   * Rasterizes the vector graphic file at the given URL. If any exception
109
   * happens, a red circle is returned instead.
110
   *
111
   * @param url   The URL to a vector graphic file, which must include the
112
   *              protocol scheme (such as file:// or https://).
113
   * @param width The number of pixels wide to render the image. The aspect
114
   *              ratio is maintained.
115
   * @return Either the rasterized image upon success or a red circle.
116
   */
117
  public static Image rasterize( final String url, final int width ) {
118
    try {
119
      return rasterize( new URL( url ), width );
120
    } catch( final Exception e ) {
121
      return createPlaceholderImage( width );
122
    }
108123
  }
109124
110
  public static BufferedImage rasterize( final URL url, final int width )
125
  /**
126
   * Converts an SVG drawing into a rasterized image that can be drawn on
127
   * a graphics context.
128
   *
129
   * @param url   The path to the image (can be web address).
130
   * @param width Scale the image width to this size (aspect ratio is
131
   *              maintained).
132
   * @return The vector graphic transcoded into a raster image format.
133
   * @throws IOException         Could not read the vector graphic.
134
   * @throws TranscoderException Could not convert the vector graphic to an
135
   *                             instance of {@link Image}.
136
   */
137
  public static Image rasterize( final URL url, final int width )
111138
      throws IOException, TranscoderException {
112139
    return rasterize(
113140
        (SVGDocument) mFactory.createDocument( url.toString() ), width );
114141
  }
115142
116
  public static BufferedImage rasterize(
143
  public static Image rasterize(
117144
      final SVGDocument svg, final int width ) throws TranscoderException {
118145
    final var transcoder = new BufferedImageTranscoder();
119146
    final var input = new TranscoderInput( svg );
120147
121148
    transcoder.addTranscodingHint( KEY_BACKGROUND_COLOR, WHITE );
122149
    transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
123150
    transcoder.transcode( input, null );
124151
125
    return transcoder.getBufferedImage();
152
    return transcoder.getImage();
153
  }
154
155
  @SuppressWarnings("SuspiciousNameCombination")
156
  private static Image createPlaceholderImage( final int width ) {
157
    final var image = new BufferedImage( width, width, TYPE_INT_RGB );
158
    final var graphics = (Graphics2D) image.getGraphics();
159
160
    graphics.setColor( RED );
161
    graphics.setStroke( new BasicStroke( 5 ) );
162
    graphics.drawOval( 5, 5, width / 2, width / 2 );
163
164
    return image;
126165
  }
127166
}
M src/main/java/com/scrivenvar/preview/SVGReplacedElementFactory.java
4141
4242
import java.awt.*;
43
import java.util.LinkedHashMap;
44
import java.util.Map;
45
46
import static com.scrivenvar.preview.SVGRasterizer.rasterize;
4347
4448
public class SVGReplacedElementFactory
...
5458
  private static final String HTML_IMAGE_SRC = "src";
5559
56
  public ReplacedElement createReplacedElement(
57
      final LayoutContext c, final BlockBox box, final UserAgentCallback uac,
58
      final int cssWidth, final int cssHeight ) {
59
    final Element e = box.getElement();
60
  /**
61
   * Constrain memory.
62
   */
63
  private static final int MAX_CACHED_IMAGES = 100;
6064
61
    if( e == null ) {
62
      return null;
65
  /**
66
   * Where to put document inline evaluated R expressions.
67
   */
68
  private final Map<String, Image> mImageCache = new LinkedHashMap<>() {
69
    @Override
70
    protected boolean removeEldestEntry(
71
        final Map.Entry<String, Image> eldest ) {
72
      return size() > MAX_CACHED_IMAGES;
6373
    }
74
  };
6475
65
    final String nodeName = e.getNodeName();
66
    ReplacedElement result = null;
76
  public ReplacedElement createReplacedElement(
77
      final LayoutContext c,
78
      final BlockBox box,
79
      final UserAgentCallback uac,
80
      final int cssWidth,
81
      final int cssHeight ) {
82
    final Element e = box.getElement();
6783
68
    if( HTML_IMAGE.equals( nodeName ) ) {
69
      final String src = e.getAttribute( HTML_IMAGE_SRC );
70
      final String ext = FilenameUtils.getExtension( src );
84
    if( e != null ) {
85
      final String nodeName = e.getNodeName();
7186
72
      if( SVG_FILE.equalsIgnoreCase( ext ) ) {
73
        try {
74
          final int width = box.getContentWidth();
75
          final Image image = SVGRasterizer.rasterize( src, width );
87
      if( HTML_IMAGE.equals( nodeName ) ) {
88
        final String src = e.getAttribute( HTML_IMAGE_SRC );
89
        final String ext = FilenameUtils.getExtension( src );
7690
77
          final int w = image.getWidth( null );
78
          final int h = image.getHeight( null );
91
        if( SVG_FILE.equalsIgnoreCase( ext ) ) {
92
          try {
93
            final int width = box.getContentWidth();
94
            final Image image = getImage( src, width );
7995
80
          result = new ImageReplacedElement( image, w, h );
81
        } catch( final Exception ex ) {
82
          getNotifier().notify( ex );
96
            final int w = image.getWidth( null );
97
            final int h = image.getHeight( null );
98
99
            return new ImageReplacedElement( image, w, h );
100
          } catch( final Exception ex ) {
101
            getNotifier().notify( ex );
102
          }
83103
        }
84104
      }
85105
    }
86106
87
    return result;
107
    return null;
88108
  }
89109
90110
  @Override
91111
  public void reset() {
92112
  }
93113
94114
  @Override
95
  public void remove( Element e ) {
115
  public void remove( final Element e ) {
96116
  }
97117
98118
  @Override
99119
  public void setFormSubmissionListener( FormSubmissionListener listener ) {
120
  }
121
122
  private Image getImage( final String src, final int width ) {
123
    return mImageCache.computeIfAbsent( src, v -> rasterize( src, width ) );
100124
  }
101125
M src/main/java/com/scrivenvar/processors/InlineRProcessor.java
6363
6464
  /**
65
   * Only one editor is open at a time.
66
   */
67
  private static final ScriptEngine ENGINE =
68
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
69
70
  /**
7165
   * Where to put document inline evaluated R expressions.
7266
   */
7367
  private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
7468
    @Override
7569
    protected boolean removeEldestEntry(
7670
        final Map.Entry<String, Object> eldest ) {
7771
      return size() > MAX_CACHED_R_STATEMENTS;
7872
    }
7973
  };
74
75
  /**
76
   * Only one editor is open at a time.
77
   */
78
  private static final ScriptEngine ENGINE =
79
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
8080
8181
  /**
A src/main/java/com/scrivenvar/processors/markdown/BlockExtension.java
1
package com.scrivenvar.processors.markdown;
2
3
import com.vladsch.flexmark.ast.BlockQuote;
4
import com.vladsch.flexmark.html.AttributeProvider;
5
import com.vladsch.flexmark.html.AttributeProviderFactory;
6
import com.vladsch.flexmark.html.HtmlRenderer;
7
import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
8
import com.vladsch.flexmark.html.renderer.AttributablePart;
9
import com.vladsch.flexmark.html.renderer.LinkResolverContext;
10
import com.vladsch.flexmark.util.ast.Block;
11
import com.vladsch.flexmark.util.ast.Node;
12
import com.vladsch.flexmark.util.data.MutableDataHolder;
13
import com.vladsch.flexmark.util.html.MutableAttributes;
14
import org.jetbrains.annotations.NotNull;
15
16
/**
17
 * Responsible for giving most block-level elements a unique identifier
18
 * attribute. The identifier is used to coordinate scrolling.
19
 */
20
public class BlockExtension implements HtmlRenderer.HtmlRendererExtension {
21
  /**
22
   * Responsible for creating the id attribute. This class is instantiated
23
   * each time the document is rendered, thereby resetting the count to zero.
24
   */
25
  public static class IdAttributeProvider implements AttributeProvider {
26
    private int mCount;
27
28
    private static AttributeProviderFactory createFactory() {
29
      return new IndependentAttributeProviderFactory() {
30
        @Override
31
        public @NotNull AttributeProvider apply(
32
            @NotNull final LinkResolverContext context ) {
33
          return new IdAttributeProvider();
34
        }
35
      };
36
    }
37
38
    @Override
39
    public void setAttributes( @NotNull Node node,
40
                               @NotNull AttributablePart part,
41
                               @NotNull MutableAttributes attributes ) {
42
      // Blockquotes are troublesome because they can interleave blank lines
43
      // without having an equivalent blank line in the source document. That
44
      // is, in Markdown the > symbol on a line by itself will generate a blank
45
      // line in the resulting document; however, a > symbol in the text editor
46
      // does not count as a blank line. Resolving this issue is tricky.
47
      if( node instanceof Block && !(node instanceof BlockQuote) ) {
48
        attributes.addValue( "id", "para-" + mCount++ );
49
      }
50
    }
51
  }
52
53
  private BlockExtension() {
54
  }
55
56
  @Override
57
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
58
  }
59
60
  @Override
61
  public void extend( final HtmlRenderer.Builder rendererBuilder,
62
                      @NotNull final String rendererType ) {
63
    rendererBuilder.attributeProviderFactory(
64
        IdAttributeProvider.createFactory()
65
    );
66
  }
67
68
  public static BlockExtension create() {
69
    return new BlockExtension();
70
  }
71
}
172
M src/main/java/com/scrivenvar/processors/markdown/ImageLinkExtension.java
5757
 */
5858
public class ImageLinkExtension implements HtmlRenderer.HtmlRendererExtension {
59
  /**
60
   * Used for image directory preferences.
61
   */
5962
  private final static Options sOptions = Services.load( Options.class );
6063
  private final static Notifier sNotifier = Services.load( Notifier.class );
M src/main/java/com/scrivenvar/processors/markdown/MarkdownProcessor.java
3030
import com.scrivenvar.processors.AbstractProcessor;
3131
import com.scrivenvar.processors.Processor;
32
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
3233
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
3334
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
3435
import com.vladsch.flexmark.ext.tables.TablesExtension;
36
import com.vladsch.flexmark.ext.typographic.TypographicExtension;
3537
import com.vladsch.flexmark.html.HtmlRenderer;
3638
import com.vladsch.flexmark.parser.Parser;
...
6971
    super( successor );
7072
73
    // Standard extensions
7174
    final Collection<Extension> extensions = new ArrayList<>();
72
    extensions.add( TablesExtension.create() );
73
    extensions.add( SuperscriptExtension.create() );
75
    extensions.add( DefinitionExtension.create() );
7476
    extensions.add( StrikethroughSubscriptExtension.create() );
77
    extensions.add( SuperscriptExtension.create() );
78
    extensions.add( TablesExtension.create() );
79
    extensions.add( TypographicExtension.create() );
80
81
    // Allows referencing image files via relative paths and dynamic file types.
7582
    extensions.add( ImageLinkExtension.create( path ) );
83
    extensions.add( BlockExtension.create() );
7684
7785
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
M src/main/resources/com/scrivenvar/preview/webview.css
1
/*
2
This software is released under the MIT license:
3
4
Permission is hereby granted, free of charge, to any person obtaining a copy of
5
this software and associated documentation files (the "Software"), to deal in
6
the Software without restriction, including without limitation the rights to
7
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
the Software, and to permit persons to whom the Software is furnished to do so,
9
subject to the following conditions:
10
11
The above copyright notice and this permission notice shall be included in all
12
copies or substantial portions of the Software.
13
14
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
*/
21
22
/* Source: https://github.com/nicolashery/markdownpad-github */
23
24
/*  GitHub stylesheet for MarkdownPad (http://markdownpad.com) */
25
261
/* RESET
272
=============================================================================*/
28
29
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6,
30
p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn,
31
em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var,
32
b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label,
33
legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas,
34
details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output,
35
ruby, section, summary, time, mark, audio, video {
36
  margin: 0;
37
  padding: 0;
38
  border: 0;
3
html, body, div, span, applet, object, iframe,
4
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
5
a, abbr, acronym, address, big, cite, code,
6
del, dfn, em, img, ins, kbd, q, s, samp,
7
small, strike, strong, sub, sup, tt, var,
8
b, u, i, center,
9
dl, dt, dd, ol, ul, li,
10
fieldset, form, label, legend,
11
table, caption, tbody, tfoot, thead, tr, th, td,
12
article, aside, canvas, details, embed,
13
figure, figcaption, footer, header, hgroup,
14
menu, nav, output, ruby, section, summary,
15
time, mark, audio, video {
16
	margin: 0;
17
	padding: 0;
18
	border: 0;
19
	font-size: 100%;
20
	font: inherit;
21
	vertical-align: baseline;
22
}
23
ol, ul {
24
	list-style: none;
25
}
26
blockquote, q {
27
	quotes: none;
28
}
29
blockquote:before, blockquote:after,
30
q:before, q:after {
31
	content: '';
32
	content: none;
33
}
34
table {
35
	border-collapse: collapse;
36
	border-spacing: 0;
3937
}
4038
4139
/* BODY
4240
=============================================================================*/
43
4441
body {
45
  font-family: serif;
46
  font-size: 14px;
47
  line-height: 1.6;
48
  color: #333;
42
  font-family: Vollkorn, serif;
43
  font-size: 16px;
4944
  background-color: #fff;
50
  padding: 20px;
51
  max-width: 960px;
5245
  margin: 0 auto;
46
  max-width: 960px;
47
  line-height: 1.6;
48
  color: #454545;
49
  padding: 0 1em
5350
}
5451
...
6360
/* BLOCKS
6461
=============================================================================*/
65
6662
p, blockquote, ul, ol, dl, table, pre {
67
  margin: 15px 0;
63
  margin: 1em 0;
6864
}
6965
7066
/* HEADERS
7167
=============================================================================*/
72
7368
h1, h2, h3, h4, h5, h6 {
74
  margin: 20px 0 10px;
75
  padding: 0;
7669
  font-weight: bold;
77
  -webkit-font-smoothing: antialiased;
70
  margin: 1em 0 .5em;
7871
}
7972
8073
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
8174
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
8275
  font-size: inherit;
8376
}
8477
8578
h1 {
8679
  font-size: 28px;
87
  color: #000;
8880
}
8981
9082
h2 {
9183
  font-size: 24px;
9284
  border-bottom: 1px solid #ccc;
93
  color: #000;
9485
}
9586
9687
h3 {
97
  font-size: 18px;
88
  font-size: 20px;
9889
}
9990
10091
h4 {
101
  font-size: 16px;
92
  font-size: 18px;
10293
}
10394
10495
h5 {
105
  font-size: 14px;
96
  font-size: 16px;
10697
}
10798
10899
h6 {
109
  color: #777;
110100
  font-size: 14px;
111
}
112
113
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2,
114
body>h3:first-child, body>h4:first-child, body>h5:first-child,
115
body>h6:first-child {
116
  margin-top: 0;
117
  padding-top: 0;
118
}
119
120
a:first-child h1, a:first-child h2, a:first-child h3,
121
a:first-child h4, a:first-child h5, a:first-child h6 {
122
  margin-top: 0;
123
  padding-top: 0;
124101
}
125102
126103
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
127
  margin-top: 10px;
104
  margin-top: .5em;
128105
}
129106
130107
/* LINKS
131108
=============================================================================*/
132
133109
a {
134
  color: #4183C4;
110
  color: #0077aa;
135111
  text-decoration: none;
136112
}
137113
138114
a:hover {
139115
  text-decoration: underline;
140116
}
141117
142
/* LISTS
118
/* BULLET LISTS
143119
=============================================================================*/
144
145120
ul, ol {
146
  padding-left: 30px;
147
}
148
149
ul li > :first-child, 
150
ol li > :first-child, 
151
ul li ul:first-of-type, 
152
ol li ol:first-of-type, 
153
ul li ol:first-of-type, 
154
ol li ul:first-of-type {
155
  margin-top: 0px;
121
  display: block;
122
  list-style: disc outside none;
123
  margin: 1em 0;
124
  padding: 0 0 0 2em;
156125
}
157126
158
ul ul, ul ol, ol ol, ol ul {
159
  margin-bottom: 0;
127
ol {
128
 list-style-type: decimal;
160129
}
161130
162
dl {
163
  padding: 0;
131
ul ul, ol ul,
132
ol ol, ul ol {
133
 list-style-position: inside;
134
 margin-left: 1em;
164135
}
165136
166
dl dt {
167
  font-size: 14px;
168
  font-weight: bold;
169
  font-style: italic;
170
  padding: 0;
171
  margin: 15px 0 5px;
137
ul ul, ol ul {
138
 list-style-type: circle;
172139
}
173140
174
dl dt:first-child {
175
  padding: 0;
141
ol ol, ul ol {
142
 list-style-type: lower-latin;
176143
}
177144
178
dl dt>:first-child {
179
  margin-top: 0px;
145
/* DEFINITION LISTS
146
=============================================================================*/
147
dl {
148
  /** Horizontal scroll bar from appears if set to 100%. */
149
  width: 99%;
150
  overflow: hidden;
151
  padding-left: 1em;
180152
}
181153
182
dl dt>:last-child {
183
  margin-bottom: 0px;
154
dl dt {
155
  font-weight: bold;
156
  float: left;
157
  width: 20%;
158
  clear: both;
159
  position: relative;
184160
}
185161
186162
dl dd {
187
  margin: 0 0 15px;
188
  padding: 0 15px;
189
}
190
191
dl dd>:first-child {
192
  margin-top: 0px;
163
  float: right;
164
  width: 79%;
165
  padding-bottom: .5em;
166
  margin-left: 0;
193167
}
194168
195
dl dd>:last-child {
196
  margin-bottom: 0px;
169
dl dd {
170
  *float: none;
171
  *width: auto;
172
  *margin-left: 20%;
197173
}
198174
199175
/* CODE
200176
=============================================================================*/
201
202177
pre, code, tt {
203178
  font-size: 12px;
204179
  font-family: Consolas, "Liberation Mono", Courier, monospace;
205180
}
206181
207182
code, tt {
208
  margin: 0 0px;
209
  padding: 0px 0px;
210183
  white-space: nowrap;
211
  border: 1px solid #eaeaea;
184
  border: 1px solid #ccc;
212185
  background-color: #f8f8f8;
213186
  border-radius: 3px;
214187
}
215188
216189
pre>code {
217
  margin: 0;
218
  padding: 0;
219190
  white-space: pre;
220191
  border: none;
221192
  background: transparent;
222193
}
223194
224195
pre {
225196
  background-color: #f8f8f8;
226
  border: 1px solid #ccc;
197
  border: .125em solid #ccc;
227198
  font-size: 13px;
228199
  line-height: 19px;
229200
  overflow: auto;
230
  padding: 6px 10px;
231
  border-radius: 3px;
201
  padding: .25em .5em;
202
  border-radius: .25em;
232203
}
233204
234205
pre code, pre tt {
235206
  background-color: transparent;
236207
  border: none;
237208
}
238209
239210
kbd {
240
  -moz-border-bottom-colors: none;
241
  -moz-border-left-colors: none;
242
  -moz-border-right-colors: none;
243
  -moz-border-top-colors: none;
244
  background-color: #DDDDDD;
211
  background-color: #ccc;
245212
  background-image: linear-gradient(#F1F1F1, #DDDDDD);
246213
  background-repeat: repeat-x;
247214
  border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
248215
  border-image: none;
249
  border-radius: 2px 2px 2px 2px;
216
  border-radius: 2px;
250217
  border-style: solid;
251218
  border-width: 1px;
252219
  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
253220
  line-height: 10px;
254221
  padding: 1px 4px;
255222
}
256223
257224
/* QUOTES
258225
=============================================================================*/
259
260226
blockquote {
261
  border-left: 4px solid #DDD;
262
  padding: 0 15px;
227
  border-left: .25em solid #ccc;
228
  padding: 0 1em;
263229
  color: #777;
264230
}
...
274240
/* HORIZONTAL RULES
275241
=============================================================================*/
276
277242
hr {
278243
  clear: both;
279
  margin: 15px 0;
244
  margin: 1.5em 0 1.5em;
280245
  height: 0px;
281246
  overflow: hidden;
282247
  border: none;
283248
  background: transparent;
284
  border-bottom: 4px solid #ddd;
285
  padding: 0;
249
  border-bottom: .125em solid #ccc;
286250
}
287251
288252
/* TABLES
289253
=============================================================================*/
290
291
table th {
292
  font-weight: bold;
254
table {
255
  width: 100%;
293256
}
294257
295
table th, table td {
296
  border: 1px solid #ccc;
297
  padding: 6px 13px;
258
tr:nth-child(odd) {
259
  background-color: #eee;
298260
}
299261
300
table tr {
301
  border-top: 1px solid #ccc;
302
  background-color: #fff;
262
th {
263
  background-color: #454545;
264
  color: #fff;
303265
}
304266
305
table tr:nth-child(2n) {
306
  background-color: #f8f8f8;
267
th, td {
268
  text-align: left;
269
  padding: 0 1em;
307270
}
308271
309272
/* IMAGES
310273
=============================================================================*/
311
312274
img {
313
  max-width: 100%
275
  max-width: 100%;
314276
}
315277
A src/main/resources/fonts/firacode/FiraCode-Bold.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-Light.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-Medium.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-Regular.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-Retina.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-SemiBold.ttf
Binary file
A src/main/resources/fonts/vollkorn/Vollkorn-Bold.ttf
Binary file
A src/main/resources/fonts/vollkorn/Vollkorn-BoldItalic.ttf
Binary file
A src/main/resources/fonts/vollkorn/Vollkorn-Italic.ttf
Binary file
A src/main/resources/fonts/vollkorn/Vollkorn-Regular.ttf
Binary file
A src/main/resources/fonts/vollkorn/VollkornSC-Bold.ttf
Binary file
A src/main/resources/fonts/vollkorn/VollkornSC-Regular.ttf
Binary file