Dave Jarvis' Repositories

M .gitignore
11
dist
2
scrivenvar.bin
3
scrivenvar.exe
2
*.bin
3
*.exe
44
build
55
.gradle
M BUILD.md
2222
After the application is compiled, run it as follows:
2323
24
    java -jar build/libs/scrivenvar.jar
24
    java -jar build/libs/keenwrite.jar
2525
2626
On Windows:
2727
28
    java -jar build\libs\scrivenvar.jar
28
    java -jar build\libs\keenwrite.jar
2929
3030
# Installers
M LICENSE.md
11
# License
22
3
Copyright 2020 White Magic Software, Ltd.
4
35
Copyright 2015 Karl Tauber
4
All rights reserved.
56
6
Copyright 2020 White Magic Software, Ltd.
77
All rights reserved.
88
M README.md
1
# ![Logo](images/logo64.png) Scrivenvar
1
# ![Logo](docs/images/app-title.png)
22
33
A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values.
44
55
## Download
66
77
Download one of the following editions:
88
9
* [Windows](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.exe)
10
* [Linux](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.bin)
11
* [Java Archive](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.jar)
9
* [Windows](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.exe)
10
* [Linux](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.bin)
11
* [Java Archive](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.jar)
1212
1313
## Run
...
2121
When upgrading to a new version, delete the following directory;
2222
23
    C:\Users\%USERNAME%\AppData\Local\warp\packages\scrivenvar.exe
23
    C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
2424
2525
### Linux
2626
27
On Linux, run `chmod +x scrivenvar.bin` then `./scrivenvar.bin`.
27
On Linux, run `chmod +x keenwrite.bin` then `./keenwrite.bin`.
2828
2929
### Other
3030
31
On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/) that includes JavaFX module support, then run:
31
On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/pages/downloads/?version=java-14#mn) that includes JavaFX module support, then run:
3232
3333
``` bash
34
java -jar scrivenvar.jar
34
java -jar keenwrite.jar
3535
```
3636
...
5151
using the application.
5252
53
## Screenshots
53
## Screenshot
5454
5555
![Screenshot with Formulas](docs/images/equations.png)
56
57
![Screenshot with Hyperlinks](docs/images/screenshot.png)
5856
5957
## License
D _config.yaml
1
---
2
application:
3
  title: "Scrivenvar"
41
M build.gradle
123123
}
124124
125
def resourceDir = sourceSets.main.resources.srcDirs[0]
126
127
def config = new Properties()
128
file("${resourceDir}/bootstrap.properties").withInputStream {
129
  config.load(it)
130
}
131
125132
application {
126
  applicationName = 'scrivenvar'
133
  applicationName = config["application.title"].toLowerCase()
127134
  mainClassName = "com.${applicationName}.Main"
128135
...
138145
def launcherClassName = "com.${applicationName}.Launcher"
139146
140
def propertiesFile = new File("src/main/resources/com/${applicationName}/app.properties")
147
def propertiesFile = new File("${resourceDir}/com/${applicationName}/app.properties")
141148
propertiesFile.write("application.version=${version}")
142149
A docs/images/app-title.png
Binary file
M docs/images/equations.png
Binary file
D docs/images/screenshot.png
Binary file
M docs/texample.Rmd
1
# ![Logo](images/app-title.png)
2
13
# Real-time equation rendering
24
3
With interpolated variables and R calculations:
5
Interpolated variables within R calculations, formatted as an equation:
46
5
$\sqrt{`r#x( v$formula$sqrt$value)`} = `r# round(sqrt(x( v$formula$sqrt$value )),5)`$
7
$\sqrt{`r#x( v$formula$sqrt$value)`} = \pm `r# round(sqrt(x( v$formula$sqrt$value )),5)`$
68
79
# Maxwell's equations
M docs/variables.yaml
22
formula:
33
  sqrt:
4
    value: "603"
4
    value: "42"
55
M installer
155155
156156
set SCRIPT_DIR=%~dp0
157
"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" -jar "%SCRIPT_DIR%\\scrivenvar.jar" %*
157
"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" -jar "%SCRIPT_DIR%\\${APP_NAME}.jar" %*
158158
__EOT
159159
A src/main/java/com/keenwrite/AbstractFileFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.keenwrite.service.Settings;
31
import com.keenwrite.util.ProtocolScheme;
32
33
import java.nio.file.Path;
34
35
import static com.keenwrite.Constants.GLOB_PREFIX_FILE;
36
import static com.keenwrite.Constants.SETTINGS;
37
import static com.keenwrite.FileType.UNKNOWN;
38
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
39
import static java.lang.String.format;
40
41
/**
42
 * Provides common behaviours for factories that instantiate classes based on
43
 * file type.
44
 */
45
public class AbstractFileFactory {
46
47
  private static final String MSG_UNKNOWN_FILE_TYPE =
48
      "Unknown type '%s' for file '%s'.";
49
50
  /**
51
   * Determines the file type from the path extension. This should only be
52
   * called when it is known that the file type won't be a definition file
53
   * (e.g., YAML or other definition source), but rather an editable file
54
   * (e.g., Markdown, XML, etc.).
55
   *
56
   * @param path The path with a file name extension.
57
   * @return The FileType for the given path.
58
   */
59
  public FileType lookup( final Path path ) {
60
    return lookup( path, GLOB_PREFIX_FILE );
61
  }
62
63
  /**
64
   * Creates a file type that corresponds to the given path.
65
   *
66
   * @param path   Reference to a variable definition file.
67
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
68
   * @return The file type that corresponds to the given path.
69
   */
70
  protected FileType lookup( final Path path, final String prefix ) {
71
    assert path != null;
72
    assert prefix != null;
73
74
    final var settings = getSettings();
75
    final var keys = settings.getKeys( prefix );
76
77
    var found = false;
78
    var fileType = UNKNOWN;
79
80
    while( keys.hasNext() && !found ) {
81
      final var key = keys.next();
82
      final var patterns = settings.getStringSettingList( key );
83
      final var predicate = createFileTypePredicate( patterns );
84
85
      if( found = predicate.test( path.toFile() ) ) {
86
        // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
87
        // to a standard name (as defined in the settings.properties file).
88
        final String suffix = key.replace( prefix + ".", "" );
89
        fileType = FileType.from( suffix );
90
      }
91
    }
92
93
    return fileType;
94
  }
95
96
  /**
97
   * Throws IllegalArgumentException because the given path could not be
98
   * recognized. This exists because
99
   *
100
   * @param type The detected path type (protocol, file extension, etc.).
101
   * @param path The path to a source of definitions.
102
   */
103
  protected void unknownFileType(
104
      final ProtocolScheme type, final String path ) {
105
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
106
    throw new IllegalArgumentException( msg );
107
  }
108
109
  /**
110
   * Return the singleton Settings instance.
111
   *
112
   * @return A non-null instance.
113
   */
114
  private Settings getSettings() {
115
    return SETTINGS;
116
  }
117
}
1118
A src/main/java/com/keenwrite/Bootstrap.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import java.io.IOException;
31
import java.util.Properties;
32
33
/**
34
 * Responsible for loading the bootstrap.properties file, which is
35
 * tactically located outside of the standard resource reverse domain name
36
 * namespace to avoid hard-coding the application name in many places.
37
 * Instead, the application name is located in the bootstrap file, which is
38
 * then used to look-up the remaining settings.
39
 * <p>
40
 * See {@link Constants#PATH_PROPERTIES_SETTINGS} for details.
41
 * </p>
42
 */
43
public class Bootstrap {
44
  private static final Properties BOOTSTRAP = new Properties();
45
46
  static {
47
    try( final var stream =
48
             Constants.class.getResourceAsStream( "/bootstrap.properties" ) ) {
49
      BOOTSTRAP.load( stream );
50
    } catch( final IOException ignored ) {
51
      // Bootstrap properties cannot be found, throw in the towel.
52
    }
53
  }
54
55
  public static final String APP_TITLE =
56
      BOOTSTRAP.getProperty( "application.title" );
57
  public static final String APP_TITLE_LOWERCASE = APP_TITLE.toLowerCase();
58
}
159
A src/main/java/com/keenwrite/Constants.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.keenwrite.service.Settings;
31
import javafx.scene.image.Image;
32
33
import java.nio.file.Path;
34
import java.nio.file.Paths;
35
36
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
37
import static java.lang.String.format;
38
39
/**
40
 * Defines application-wide default values.
41
 */
42
public class Constants {
43
44
  /**
45
   * Used by the default settings to load the {@link Settings} service. This
46
   * must come before any attempt is made to create a {@link Settings} object.
47
   * The reference to {@link Bootstrap#APP_TITLE_LOWERCASE} should cause the
48
   * JVM to load {@link Bootstrap} prior to proceeding. Loading that class
49
   * beforehand will read the bootstrap properties file to determine the
50
   * application name, which is then used to locate the settings properties.
51
   */
52
  public static final String PATH_PROPERTIES_SETTINGS =
53
      format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE );
54
55
  /**
56
   * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}.
57
   */
58
  public static final Settings SETTINGS = Services.load( Settings.class );
59
60
  public static final String DEFINITION_NAME = "variables.yaml";
61
62
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
63
64
  // Prevent double events when updating files on Linux (save and timestamp).
65
  public static final int APP_WATCHDOG_TIMEOUT = get(
66
      "application.watchdog.timeout", 200 );
67
68
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
69
  public static final String STYLESHEET_MARKDOWN = get(
70
      "file.stylesheet.markdown" );
71
  public static final String STYLESHEET_PREVIEW = get(
72
      "file.stylesheet.preview" );
73
74
  public static final String FILE_LOGO_16 = get( "file.logo.16" );
75
  public static final String FILE_LOGO_32 = get( "file.logo.32" );
76
  public static final String FILE_LOGO_128 = get( "file.logo.128" );
77
  public static final String FILE_LOGO_256 = get( "file.logo.256" );
78
  public static final String FILE_LOGO_512 = get( "file.logo.512" );
79
80
  public static final Image ICON_DIALOG = new Image( FILE_LOGO_32 );
81
82
  public static final String PREFS_ROOT = get( "preferences.root" );
83
  public static final String PREFS_STATE = get( "preferences.root.state" );
84
85
  /**
86
   * Refer to filename extension settings in the configuration file. Do not
87
   * terminate these prefixes with a period.
88
   */
89
  public static final String GLOB_PREFIX_FILE = "file.ext";
90
  public static final String GLOB_PREFIX_DEFINITION =
91
      "definition." + GLOB_PREFIX_FILE;
92
93
  /**
94
   * Three parameters: line number, column number, and offset.
95
   */
96
  public static final String STATUS_BAR_LINE = "Main.status.line";
97
98
  public static final String STATUS_BAR_OK = "Main.status.state.default";
99
100
  /**
101
   * Used to show an error while parsing, usually syntactical.
102
   */
103
  public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
104
  public static final String STATUS_DEFINITION_BLANK =
105
      "Main.status.error.def.blank";
106
  public static final String STATUS_DEFINITION_EMPTY =
107
      "Main.status.error.def.empty";
108
109
  /**
110
   * One parameter: the word under the cursor that could not be found.
111
   */
112
  public static final String STATUS_DEFINITION_MISSING =
113
      "Main.status.error.def.missing";
114
115
  /**
116
   * Used when creating flat maps relating to resolved variables.
117
   */
118
  public static final int DEFAULT_MAP_SIZE = 64;
119
120
  /**
121
   * Default image extension order to use when scanning.
122
   */
123
  public static final String PERSIST_IMAGES_DEFAULT =
124
      get( "file.ext.image.order" );
125
126
  /**
127
   * Default working directory to use for R startup script.
128
   */
129
  public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
130
131
  /**
132
   * Default path to use for an untitled (pathless) file.
133
   */
134
  public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY );
135
136
  /**
137
   * Default starting delimiter for definition variables.
138
   */
139
  public static final String DEF_DELIM_BEGAN_DEFAULT = "${";
140
141
  /**
142
   * Default ending delimiter for definition variables.
143
   */
144
  public static final String DEF_DELIM_ENDED_DEFAULT = "}";
145
146
  /**
147
   * Default starting delimiter when inserting R variables.
148
   */
149
  public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
150
151
  /**
152
   * Default ending delimiter when inserting R variables.
153
   */
154
  public static final String R_DELIM_ENDED_DEFAULT = " )";
155
156
  /**
157
   * Resource directory where different language lexicons are located.
158
   */
159
  public static final String LEXICONS_DIRECTORY = "lexicons";
160
161
  /**
162
   * Used as the prefix for uniquely identifying HTML block elements, which
163
   * helps coordinate scrolling the preview pane to where the user is typing.
164
   */
165
  public static final String PARAGRAPH_ID_PREFIX = "p-";
166
167
  /**
168
   * Absolute location of true type font files within the Java archive file.
169
   */
170
  public static final String FONT_DIRECTORY = "/fonts";
171
172
  /**
173
   * Default text editor font size, in points.
174
   */
175
  public static final float FONT_SIZE_EDITOR = 12f;
176
177
  /**
178
   * Prevent instantiation.
179
   */
180
  private Constants() {
181
  }
182
183
  private static String get( final String key ) {
184
    return SETTINGS.getSetting( key, "" );
185
  }
186
187
  @SuppressWarnings("SameParameterValue")
188
  private static int get( final String key, final int defaultValue ) {
189
    return SETTINGS.getSetting( key, defaultValue );
190
  }
191
}
1192
A src/main/java/com/keenwrite/FileEditorTab.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * Redistribution and use in source and binary forms, with or without
5
 * modification, are permitted provided that the following conditions are met:
6
 *
7
 *  o Redistributions of source code must retain the above copyright
8
 *    notice, this list of conditions and the following disclaimer.
9
 *
10
 *  o Redistributions in binary form must reproduce the above copyright
11
 *    notice, this list of conditions and the following disclaimer in the
12
 *    documentation and/or other materials provided with the distribution.
13
 *
14
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
 */
26
package com.keenwrite;
27
28
import com.keenwrite.editors.EditorPane;
29
import com.keenwrite.editors.markdown.MarkdownEditorPane;
30
import com.keenwrite.service.events.Notification;
31
import com.keenwrite.service.events.Notifier;
32
import javafx.beans.binding.Bindings;
33
import javafx.beans.property.BooleanProperty;
34
import javafx.beans.property.ReadOnlyBooleanProperty;
35
import javafx.beans.property.ReadOnlyBooleanWrapper;
36
import javafx.beans.property.SimpleBooleanProperty;
37
import javafx.beans.value.ChangeListener;
38
import javafx.event.Event;
39
import javafx.event.EventHandler;
40
import javafx.event.EventType;
41
import javafx.scene.Scene;
42
import javafx.scene.control.Tab;
43
import javafx.scene.control.Tooltip;
44
import javafx.scene.text.Text;
45
import javafx.stage.Window;
46
import org.fxmisc.flowless.VirtualizedScrollPane;
47
import org.fxmisc.richtext.StyleClassedTextArea;
48
import org.fxmisc.undo.UndoManager;
49
import org.jetbrains.annotations.NotNull;
50
import org.mozilla.universalchardet.UniversalDetector;
51
52
import java.io.File;
53
import java.nio.charset.Charset;
54
import java.nio.file.Files;
55
import java.nio.file.Path;
56
57
import static com.keenwrite.Messages.get;
58
import static com.keenwrite.StatusBarNotifier.alert;
59
import static com.keenwrite.StatusBarNotifier.getNotifier;
60
import static java.nio.charset.StandardCharsets.UTF_8;
61
import static java.util.Locale.ENGLISH;
62
import static javafx.application.Platform.runLater;
63
64
/**
65
 * Editor for a single file.
66
 */
67
public final class FileEditorTab extends Tab {
68
69
  private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane();
70
71
  private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
72
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
73
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
74
75
  /**
76
   * Character encoding used by the file (or default encoding if none found).
77
   */
78
  private Charset mEncoding = UTF_8;
79
80
  /**
81
   * File to load into the editor.
82
   */
83
  private Path mPath;
84
85
  public FileEditorTab( final Path path ) {
86
    setPath( path );
87
88
    mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
89
90
    setOnSelectionChanged( e -> {
91
      if( isSelected() ) {
92
        runLater( this::activated );
93
        requestFocus();
94
      }
95
    } );
96
  }
97
98
  private void updateTab() {
99
    setText( getTabTitle() );
100
    setGraphic( getModifiedMark() );
101
    setTooltip( getTabTooltip() );
102
  }
103
104
  /**
105
   * Returns the base filename (without the directory names).
106
   *
107
   * @return The untitled text if the path hasn't been set.
108
   */
109
  private String getTabTitle() {
110
    return getPath().getFileName().toString();
111
  }
112
113
  /**
114
   * Returns the full filename represented by the path.
115
   *
116
   * @return The untitled text if the path hasn't been set.
117
   */
118
  private Tooltip getTabTooltip() {
119
    final Path filePath = getPath();
120
    return new Tooltip( filePath == null ? "" : filePath.toString() );
121
  }
122
123
  /**
124
   * Returns a marker to indicate whether the file has been modified.
125
   *
126
   * @return "*" when the file has changed; otherwise null.
127
   */
128
  private Text getModifiedMark() {
129
    return isModified() ? new Text( "*" ) : null;
130
  }
131
132
  /**
133
   * Called when the user switches tab.
134
   */
135
  private void activated() {
136
    // Tab is closed or no longer active.
137
    if( getTabPane() == null || !isSelected() ) {
138
      return;
139
    }
140
141
    // If the tab is devoid of content, load it.
142
    if( getContent() == null ) {
143
      readFile();
144
      initLayout();
145
      initUndoManager();
146
    }
147
  }
148
149
  private void initLayout() {
150
    setContent( getScrollPane() );
151
  }
152
153
  /**
154
   * Tracks undo requests, but can only be called <em>after</em> load.
155
   */
156
  private void initUndoManager() {
157
    final UndoManager<?> undoManager = getUndoManager();
158
    undoManager.forgetHistory();
159
160
    // Bind the editor undo manager to the properties.
161
    mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
162
    canUndo.bind( undoManager.undoAvailableProperty() );
163
    canRedo.bind( undoManager.redoAvailableProperty() );
164
  }
165
166
  private void requestFocus() {
167
    getEditorPane().requestFocus();
168
  }
169
170
  /**
171
   * Searches from the caret position forward for the given string.
172
   *
173
   * @param needle The text string to match.
174
   */
175
  public void searchNext( final String needle ) {
176
    final String haystack = getEditorText();
177
    int index = haystack.indexOf( needle, getCaretPosition() );
178
179
    // Wrap around.
180
    if( index == -1 ) {
181
      index = haystack.indexOf( needle );
182
    }
183
184
    if( index >= 0 ) {
185
      setCaretPosition( index );
186
      getEditor().selectRange( index, index + needle.length() );
187
    }
188
  }
189
190
  /**
191
   * Gets a reference to the scroll pane that houses the editor.
192
   *
193
   * @return The editor's scroll pane, containing a vertical scrollbar.
194
   */
195
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
196
    return getEditorPane().getScrollPane();
197
  }
198
199
  /**
200
   * Returns the index into the text where the caret blinks happily away.
201
   *
202
   * @return A number from 0 to the editor's document text length.
203
   */
204
  public int getCaretPosition() {
205
    return getEditor().getCaretPosition();
206
  }
207
208
  /**
209
   * Moves the caret to a given offset.
210
   *
211
   * @param offset The new caret offset.
212
   */
213
  private void setCaretPosition( final int offset ) {
214
    getEditor().moveTo( offset );
215
    getEditor().requestFollowCaret();
216
  }
217
218
  /**
219
   * Returns the text area associated with this tab.
220
   *
221
   * @return A text editor.
222
   */
223
  private StyleClassedTextArea getEditor() {
224
    return getEditorPane().getEditor();
225
  }
226
227
  /**
228
   * Returns true if the given path exactly matches this tab's path.
229
   *
230
   * @param check The path to compare against.
231
   * @return true The paths are the same.
232
   */
233
  public boolean isPath( final Path check ) {
234
    final Path filePath = getPath();
235
236
    return filePath != null && filePath.equals( check );
237
  }
238
239
  /**
240
   * Reads the entire file contents from the path associated with this tab.
241
   */
242
  private void readFile() {
243
    final Path path = getPath();
244
    final File file = path.toFile();
245
246
    try {
247
      if( file.exists() ) {
248
        if( file.canWrite() && file.canRead() ) {
249
          final EditorPane pane = getEditorPane();
250
          pane.setText( asString( Files.readAllBytes( path ) ) );
251
          pane.scrollToTop();
252
        }
253
        else {
254
          final String msg = get( "FileEditor.loadFailed.reason.permissions" );
255
          alert( "FileEditor.loadFailed.message", file.toString(), msg );
256
        }
257
      }
258
    } catch( final Exception ex ) {
259
      alert( ex );
260
    }
261
  }
262
263
  /**
264
   * Saves the entire file contents from the path associated with this tab.
265
   *
266
   * @return true The file has been saved.
267
   */
268
  public boolean save() {
269
    try {
270
      final EditorPane editor = getEditorPane();
271
      Files.write( getPath(), asBytes( editor.getText() ) );
272
      editor.getUndoManager().mark();
273
      return true;
274
    } catch( final Exception ex ) {
275
      return popupAlert(
276
          "FileEditor.saveFailed.title",
277
          "FileEditor.saveFailed.message",
278
          ex
279
      );
280
    }
281
  }
282
283
  /**
284
   * Creates an alert dialog and waits for it to close.
285
   *
286
   * @param titleKey   Resource bundle key for the alert dialog title.
287
   * @param messageKey Resource bundle key for the alert dialog message.
288
   * @param e          The unexpected happening.
289
   * @return false
290
   */
291
  @SuppressWarnings("SameParameterValue")
292
  private boolean popupAlert(
293
      final String titleKey, final String messageKey, final Exception e ) {
294
    final Notifier service = getNotifier();
295
    final Path filePath = getPath();
296
297
    final Notification message = service.createNotification(
298
        get( titleKey ),
299
        get( messageKey ),
300
        filePath == null ? "" : filePath,
301
        e.getMessage()
302
    );
303
304
    try {
305
      service.createError( getWindow(), message ).showAndWait();
306
    } catch( final Exception ex ) {
307
      alert( ex );
308
    }
309
310
    return false;
311
  }
312
313
  private Window getWindow() {
314
    final Scene scene = getEditorPane().getScene();
315
316
    if( scene == null ) {
317
      throw new UnsupportedOperationException( "No scene window available" );
318
    }
319
320
    return scene.getWindow();
321
  }
322
323
  /**
324
   * Returns a best guess at the file encoding. If the encoding could not be
325
   * detected, this will return the default charset for the JVM.
326
   *
327
   * @param bytes The bytes to perform character encoding detection.
328
   * @return The character encoding.
329
   */
330
  private Charset detectEncoding( final byte[] bytes ) {
331
    final var detector = new UniversalDetector( null );
332
    detector.handleData( bytes, 0, bytes.length );
333
    detector.dataEnd();
334
335
    final String charset = detector.getDetectedCharset();
336
337
    return charset == null
338
        ? Charset.defaultCharset()
339
        : Charset.forName( charset.toUpperCase( ENGLISH ) );
340
  }
341
342
  /**
343
   * Converts the given string to an array of bytes using the encoding that was
344
   * originally detected (if any) and associated with this file.
345
   *
346
   * @param text The text to convert into the original file encoding.
347
   * @return A series of bytes ready for writing to a file.
348
   */
349
  private byte[] asBytes( final String text ) {
350
    return text.getBytes( getEncoding() );
351
  }
352
353
  /**
354
   * Converts the given bytes into a Java String. This will call setEncoding
355
   * with the encoding detected by the CharsetDetector.
356
   *
357
   * @param text The text of unknown character encoding.
358
   * @return The text, in its auto-detected encoding, as a String.
359
   */
360
  private String asString( final byte[] text ) {
361
    setEncoding( detectEncoding( text ) );
362
    return new String( text, getEncoding() );
363
  }
364
365
  /**
366
   * Returns the path to the file being edited in this tab.
367
   *
368
   * @return A non-null instance.
369
   */
370
  public Path getPath() {
371
    return mPath;
372
  }
373
374
  /**
375
   * Sets the path to a file for editing and then updates the tab with the
376
   * file contents.
377
   *
378
   * @param path A non-null instance.
379
   */
380
  public void setPath( final Path path ) {
381
    assert path != null;
382
    mPath = path;
383
384
    updateTab();
385
  }
386
387
  public boolean isModified() {
388
    return mModified.get();
389
  }
390
391
  ReadOnlyBooleanProperty modifiedProperty() {
392
    return mModified.getReadOnlyProperty();
393
  }
394
395
  BooleanProperty canUndoProperty() {
396
    return this.canUndo;
397
  }
398
399
  BooleanProperty canRedoProperty() {
400
    return this.canRedo;
401
  }
402
403
  private UndoManager<?> getUndoManager() {
404
    return getEditorPane().getUndoManager();
405
  }
406
407
  /**
408
   * Forwards to the editor pane's listeners for text change events.
409
   *
410
   * @param listener The listener to notify when the text changes.
411
   */
412
  public void addTextChangeListener( final ChangeListener<String> listener ) {
413
    getEditorPane().addTextChangeListener( listener );
414
  }
415
416
  /**
417
   * Forwards to the editor pane's listeners for caret change events.
418
   *
419
   * @param listener Notified when the caret position changes.
420
   */
421
  public void addCaretPositionListener(
422
      final ChangeListener<? super Integer> listener ) {
423
    getEditorPane().addCaretPositionListener( listener );
424
  }
425
426
  /**
427
   * Forwards to the editor pane's listeners for paragraph index change events.
428
   *
429
   * @param listener Notified when the caret's paragraph index changes.
430
   */
431
  public void addCaretParagraphListener(
432
      final ChangeListener<? super Integer> listener ) {
433
    getEditorPane().addCaretParagraphListener( listener );
434
  }
435
436
  public <T extends Event> void addEventFilter(
437
      final EventType<T> eventType,
438
      final EventHandler<? super T> eventFilter ) {
439
    getEditor().addEventFilter( eventType, eventFilter );
440
  }
441
442
  /**
443
   * Forwards the request to the editor pane.
444
   *
445
   * @return The text to process.
446
   */
447
  public String getEditorText() {
448
    return getEditorPane().getText();
449
  }
450
451
  /**
452
   * Returns the editor pane, or creates one if it doesn't yet exist.
453
   *
454
   * @return The editor pane, never null.
455
   */
456
  @NotNull
457
  public MarkdownEditorPane getEditorPane() {
458
    return mEditorPane;
459
  }
460
461
  /**
462
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
463
   * determined.
464
   *
465
   * @return The file encoding or UTF-8 if unknown.
466
   */
467
  private Charset getEncoding() {
468
    return mEncoding;
469
  }
470
471
  private void setEncoding( final Charset encoding ) {
472
    assert encoding != null;
473
    mEncoding = encoding;
474
  }
475
476
  /**
477
   * Returns the tab title, without any modified indicators.
478
   *
479
   * @return The tab title.
480
   */
481
  @Override
482
  public String toString() {
483
    return getTabTitle();
484
  }
485
}
1486
A src/main/java/com/keenwrite/FileEditorTabPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.keenwrite.service.Options;
31
import com.keenwrite.service.Settings;
32
import com.keenwrite.service.events.Notification;
33
import com.keenwrite.service.events.Notifier;
34
import com.keenwrite.util.Utils;
35
import javafx.beans.property.ReadOnlyBooleanProperty;
36
import javafx.beans.property.ReadOnlyBooleanWrapper;
37
import javafx.beans.property.ReadOnlyObjectProperty;
38
import javafx.beans.property.ReadOnlyObjectWrapper;
39
import javafx.beans.value.ChangeListener;
40
import javafx.collections.ListChangeListener;
41
import javafx.collections.ObservableList;
42
import javafx.event.Event;
43
import javafx.scene.control.Alert;
44
import javafx.scene.control.ButtonType;
45
import javafx.scene.control.Tab;
46
import javafx.scene.control.TabPane;
47
import javafx.stage.FileChooser;
48
import javafx.stage.FileChooser.ExtensionFilter;
49
import javafx.stage.Window;
50
51
import java.io.File;
52
import java.nio.file.Path;
53
import java.util.ArrayList;
54
import java.util.List;
55
import java.util.Optional;
56
import java.util.concurrent.atomic.AtomicReference;
57
import java.util.prefs.Preferences;
58
import java.util.stream.Collectors;
59
60
import static com.keenwrite.Constants.GLOB_PREFIX_FILE;
61
import static com.keenwrite.Constants.SETTINGS;
62
import static com.keenwrite.FileType.*;
63
import static com.keenwrite.Messages.get;
64
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
65
import static com.keenwrite.service.events.Notifier.YES;
66
67
/**
68
 * Tab pane for file editors.
69
 */
70
public final class FileEditorTabPane extends TabPane {
71
72
  private static final String FILTER_EXTENSION_TITLES =
73
      "Dialog.file.choose.filter";
74
75
  private static final Options sOptions = Services.load( Options.class );
76
  private static final Notifier sNotifier = Services.load( Notifier.class );
77
78
  private final ReadOnlyObjectWrapper<Path> mOpenDefinition =
79
      new ReadOnlyObjectWrapper<>();
80
  private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
81
      new ReadOnlyObjectWrapper<>();
82
  private final ReadOnlyBooleanWrapper mAnyFileEditorModified =
83
      new ReadOnlyBooleanWrapper();
84
  private final ChangeListener<Integer> mCaretPositionListener;
85
  private final ChangeListener<Integer> mCaretParagraphListener;
86
87
  /**
88
   * Constructs a new file editor tab pane.
89
   *
90
   * @param caretPositionListener  Listens for changes to caret position so
91
   *                               that the status bar can update.
92
   * @param caretParagraphListener Listens for changes to the caret's paragraph
93
   *                               so that scrolling may occur.
94
   */
95
  public FileEditorTabPane(
96
      final ChangeListener<Integer> caretPositionListener,
97
      final ChangeListener<Integer> caretParagraphListener ) {
98
    final ObservableList<Tab> tabs = getTabs();
99
100
    setFocusTraversable( false );
101
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
102
103
    addTabSelectionListener(
104
        ( tabPane, oldTab, newTab ) -> {
105
          if( newTab != null ) {
106
            mActiveFileEditor.set( (FileEditorTab) newTab );
107
          }
108
        }
109
    );
110
111
    final ChangeListener<Boolean> modifiedListener =
112
        ( observable, oldValue, newValue ) -> {
113
          for( final Tab tab : tabs ) {
114
            if( ((FileEditorTab) tab).isModified() ) {
115
              mAnyFileEditorModified.set( true );
116
              break;
117
            }
118
          }
119
        };
120
121
    tabs.addListener(
122
        (ListChangeListener<Tab>) change -> {
123
          while( change.next() ) {
124
            if( change.wasAdded() ) {
125
              change.getAddedSubList().forEach(
126
                  ( tab ) -> {
127
                    final var fet = (FileEditorTab) tab;
128
                    fet.modifiedProperty().addListener( modifiedListener );
129
                  } );
130
            }
131
            else if( change.wasRemoved() ) {
132
              change.getRemoved().forEach(
133
                  ( tab ) -> {
134
                    final var fet = (FileEditorTab) tab;
135
                    fet.modifiedProperty().removeListener( modifiedListener );
136
                  }
137
              );
138
            }
139
          }
140
141
          // Changes in the tabs may also change anyFileEditorModified property
142
          // (e.g. closed modified file)
143
          modifiedListener.changed( null, null, null );
144
        }
145
    );
146
147
    mCaretPositionListener = caretPositionListener;
148
    mCaretParagraphListener = caretParagraphListener;
149
  }
150
151
  /**
152
   * Allows observers to be notified when the current file editor tab changes.
153
   *
154
   * @param listener The listener to notify of tab change events.
155
   */
156
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
157
    // Observe the tab so that when a new tab is opened or selected,
158
    // a notification is kicked off.
159
    getSelectionModel().selectedItemProperty().addListener( listener );
160
  }
161
162
  /**
163
   * Returns the tab that has keyboard focus.
164
   *
165
   * @return A non-null instance.
166
   */
167
  public FileEditorTab getActiveFileEditor() {
168
    return mActiveFileEditor.get();
169
  }
170
171
  /**
172
   * Returns the property corresponding to the tab that has focus.
173
   *
174
   * @return A non-null instance.
175
   */
176
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
177
    return mActiveFileEditor.getReadOnlyProperty();
178
  }
179
180
  /**
181
   * Property that can answer whether the text has been modified.
182
   *
183
   * @return A non-null instance, true meaning the content has not been saved.
184
   */
185
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
186
    return mAnyFileEditorModified.getReadOnlyProperty();
187
  }
188
189
  /**
190
   * Creates a new editor instance from the given path.
191
   *
192
   * @param path The file to open.
193
   * @return A non-null instance.
194
   */
195
  private FileEditorTab createFileEditor( final Path path ) {
196
    assert path != null;
197
198
    final FileEditorTab tab = new FileEditorTab( path );
199
200
    tab.setOnCloseRequest( e -> {
201
      if( !canCloseEditor( tab ) ) {
202
        e.consume();
203
      }
204
      else if( isActiveFileEditor( tab ) ) {
205
        // Prevent prompting the user to save when there are no file editor
206
        // tabs open.
207
        mActiveFileEditor.set( null );
208
      }
209
    } );
210
211
    tab.addCaretPositionListener( mCaretPositionListener );
212
    tab.addCaretParagraphListener( mCaretParagraphListener );
213
214
    return tab;
215
  }
216
217
  private boolean isActiveFileEditor( final FileEditorTab tab ) {
218
    return getActiveFileEditor() == tab;
219
  }
220
221
  private Path getDefaultPath() {
222
    final String filename = getDefaultFilename();
223
    return (new File( filename )).toPath();
224
  }
225
226
  private String getDefaultFilename() {
227
    return getSettings().getSetting( "file.default", "untitled.md" );
228
  }
229
230
  /**
231
   * Called to add a new {@link FileEditorTab} to the tab pane.
232
   */
233
  void newEditor() {
234
    final FileEditorTab tab = createFileEditor( getDefaultPath() );
235
236
    getTabs().add( tab );
237
    getSelectionModel().select( tab );
238
  }
239
240
  void openFileDialog() {
241
    final String title = get( "Dialog.file.choose.open.title" );
242
    final FileChooser dialog = createFileChooser( title );
243
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
244
245
    if( files != null ) {
246
      openFiles( files );
247
    }
248
  }
249
250
  /**
251
   * Opens the files into new editors, unless one of those files was a
252
   * definition file. The definition file is loaded into the definition pane,
253
   * but only the first one selected (multiple definition files will result in a
254
   * warning).
255
   *
256
   * @param files The list of non-definition files that the were requested to
257
   *              open.
258
   */
259
  private void openFiles( final List<File> files ) {
260
    final List<String> extensions =
261
        createExtensionFilter( DEFINITION ).getExtensions();
262
    final var predicate = createFileTypePredicate( extensions );
263
264
    // The user might have opened multiple definitions files. These will
265
    // be discarded from the text editable files.
266
    final var definitions
267
        = files.stream().filter( predicate ).collect( Collectors.toList() );
268
269
    // Create a modifiable list to remove any definition files that were
270
    // opened.
271
    final var editors = new ArrayList<>( files );
272
273
    if( !editors.isEmpty() ) {
274
      saveLastDirectory( editors.get( 0 ) );
275
    }
276
277
    editors.removeAll( definitions );
278
279
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
280
    if( !editors.isEmpty() ) {
281
      openEditors( editors, 0 );
282
    }
283
284
    if( !definitions.isEmpty() ) {
285
      openDefinition( definitions.get( 0 ) );
286
    }
287
  }
288
289
  private void openEditors( final List<File> files, final int activeIndex ) {
290
    final int fileTally = files.size();
291
    final List<Tab> tabs = getTabs();
292
293
    // Close single unmodified "Untitled" tab.
294
    if( tabs.size() == 1 ) {
295
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
296
297
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
298
        closeEditor( fileEditor, false );
299
      }
300
    }
301
302
    for( int i = 0; i < fileTally; i++ ) {
303
      final Path path = files.get( i ).toPath();
304
305
      FileEditorTab fileEditorTab = findEditor( path );
306
307
      // Only open new files.
308
      if( fileEditorTab == null ) {
309
        fileEditorTab = createFileEditor( path );
310
        getTabs().add( fileEditorTab );
311
      }
312
313
      // Select the first file in the list.
314
      if( i == activeIndex ) {
315
        getSelectionModel().select( fileEditorTab );
316
      }
317
    }
318
  }
319
320
  /**
321
   * Returns a property that changes when a new definition file is opened.
322
   *
323
   * @return The path to a definition file that was opened.
324
   */
325
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
326
    return getOnOpenDefinitionFile().getReadOnlyProperty();
327
  }
328
329
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
330
    return mOpenDefinition;
331
  }
332
333
  /**
334
   * Called when the user has opened a definition file (using the file open
335
   * dialog box). This will replace the current set of definitions for the
336
   * active tab.
337
   *
338
   * @param definition The file to open.
339
   */
340
  private void openDefinition( final File definition ) {
341
    // TODO: Prevent reading this file twice when a new text document is opened.
342
    // (might be a matter of checking the value first).
343
    getOnOpenDefinitionFile().set( definition.toPath() );
344
  }
345
346
  /**
347
   * Called when the contents of the editor are to be saved.
348
   *
349
   * @param tab The tab containing content to save.
350
   * @return true The contents were saved (or needn't be saved).
351
   */
352
  public boolean saveEditor( final FileEditorTab tab ) {
353
    if( tab == null || !tab.isModified() ) {
354
      return true;
355
    }
356
357
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
358
  }
359
360
  /**
361
   * Opens the Save As dialog for the user to save the content under a new
362
   * path.
363
   *
364
   * @param tab The tab with contents to save.
365
   * @return true The contents were saved, or the tab was null.
366
   */
367
  public boolean saveEditorAs( final FileEditorTab tab ) {
368
    if( tab == null ) {
369
      return true;
370
    }
371
372
    getSelectionModel().select( tab );
373
374
    final FileChooser fileChooser = createFileChooser( get(
375
        "Dialog.file.choose.save.title" ) );
376
    final File file = fileChooser.showSaveDialog( getWindow() );
377
    if( file == null ) {
378
      return false;
379
    }
380
381
    saveLastDirectory( file );
382
    tab.setPath( file.toPath() );
383
384
    return tab.save();
385
  }
386
387
  void saveAllEditors() {
388
    for( final FileEditorTab fileEditor : getAllEditors() ) {
389
      saveEditor( fileEditor );
390
    }
391
  }
392
393
  /**
394
   * Answers whether the file has had modifications. '
395
   *
396
   * @param tab THe tab to check for modifications.
397
   * @return false The file is unmodified.
398
   */
399
  @SuppressWarnings("BooleanMethodIsAlwaysInverted")
400
  boolean canCloseEditor( final FileEditorTab tab ) {
401
    final AtomicReference<Boolean> canClose = new AtomicReference<>();
402
    canClose.set( true );
403
404
    if( tab.isModified() ) {
405
      final Notification message = getNotifyService().createNotification(
406
          Messages.get( "Alert.file.close.title" ),
407
          Messages.get( "Alert.file.close.text" ),
408
          tab.getText()
409
      );
410
411
      final Alert confirmSave = getNotifyService().createConfirmation(
412
          getWindow(), message );
413
414
      final Optional<ButtonType> buttonType = confirmSave.showAndWait();
415
416
      buttonType.ifPresent(
417
          save -> canClose.set(
418
              save == YES ? saveEditor( tab ) : save == ButtonType.NO
419
          )
420
      );
421
    }
422
423
    return canClose.get();
424
  }
425
426
  boolean closeEditor( final FileEditorTab tab, final boolean save ) {
427
    if( tab == null ) {
428
      return true;
429
    }
430
431
    if( save ) {
432
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
433
      Event.fireEvent( tab, event );
434
435
      if( event.isConsumed() ) {
436
        return false;
437
      }
438
    }
439
440
    getTabs().remove( tab );
441
442
    if( tab.getOnClosed() != null ) {
443
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
444
    }
445
446
    return true;
447
  }
448
449
  boolean closeAllEditors() {
450
    final FileEditorTab[] allEditors = getAllEditors();
451
    final FileEditorTab activeEditor = getActiveFileEditor();
452
453
    // try to save active tab first because in case the user decides to cancel,
454
    // then it stays active
455
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
456
      return false;
457
    }
458
459
    // This should be called any time a tab changes.
460
    persistPreferences();
461
462
    // save modified tabs
463
    for( int i = 0; i < allEditors.length; i++ ) {
464
      final FileEditorTab fileEditor = allEditors[ i ];
465
466
      if( fileEditor == activeEditor ) {
467
        continue;
468
      }
469
470
      if( fileEditor.isModified() ) {
471
        // activate the modified tab to make its modified content visible to
472
        // the user
473
        getSelectionModel().select( i );
474
475
        if( !canCloseEditor( fileEditor ) ) {
476
          return false;
477
        }
478
      }
479
    }
480
481
    // Close all tabs.
482
    for( final FileEditorTab fileEditor : allEditors ) {
483
      if( !closeEditor( fileEditor, false ) ) {
484
        return false;
485
      }
486
    }
487
488
    return getTabs().isEmpty();
489
  }
490
491
  private FileEditorTab[] getAllEditors() {
492
    final ObservableList<Tab> tabs = getTabs();
493
    final int length = tabs.size();
494
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
495
496
    for( int i = 0; i < length; i++ ) {
497
      allEditors[ i ] = (FileEditorTab) tabs.get( i );
498
    }
499
500
    return allEditors;
501
  }
502
503
  /**
504
   * Returns the file editor tab that has the given path.
505
   *
506
   * @return null No file editor tab for the given path was found.
507
   */
508
  private FileEditorTab findEditor( final Path path ) {
509
    for( final Tab tab : getTabs() ) {
510
      final FileEditorTab fileEditor = (FileEditorTab) tab;
511
512
      if( fileEditor.isPath( path ) ) {
513
        return fileEditor;
514
      }
515
    }
516
517
    return null;
518
  }
519
520
  private FileChooser createFileChooser( String title ) {
521
    final FileChooser fileChooser = new FileChooser();
522
523
    fileChooser.setTitle( title );
524
    fileChooser.getExtensionFilters().addAll(
525
        createExtensionFilters() );
526
527
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
528
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
529
530
    if( !file.isDirectory() ) {
531
      file = new File( "." );
532
    }
533
534
    fileChooser.setInitialDirectory( file );
535
    return fileChooser;
536
  }
537
538
  private List<ExtensionFilter> createExtensionFilters() {
539
    final List<ExtensionFilter> list = new ArrayList<>();
540
541
    // TODO: Return a list of all properties that match the filter prefix.
542
    // This will allow dynamic filters to be added and removed just by
543
    // updating the properties file.
544
    list.add( createExtensionFilter( ALL ) );
545
    list.add( createExtensionFilter( SOURCE ) );
546
    list.add( createExtensionFilter( DEFINITION ) );
547
    list.add( createExtensionFilter( XML ) );
548
    return list;
549
  }
550
551
  /**
552
   * Returns a filter for file name extensions recognized by the application
553
   * that can be opened by the user.
554
   *
555
   * @param filetype Used to find the globbing pattern for extensions.
556
   * @return A filename filter suitable for use by a FileDialog instance.
557
   */
558
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
559
    final String tKey = String.format( "%s.title.%s",
560
                                       FILTER_EXTENSION_TITLES,
561
                                       filetype );
562
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
563
564
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
565
  }
566
567
  private void saveLastDirectory( final File file ) {
568
    getPreferences().put( "lastDirectory", file.getParent() );
569
  }
570
571
  public void initPreferences() {
572
    int activeIndex = 0;
573
574
    final Preferences preferences = getPreferences();
575
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
576
    final String activeFileName = preferences.get( "activeFile", null );
577
578
    final List<File> files = new ArrayList<>( fileNames.length );
579
580
    for( final String fileName : fileNames ) {
581
      final File file = new File( fileName );
582
583
      if( file.exists() ) {
584
        files.add( file );
585
586
        if( fileName.equals( activeFileName ) ) {
587
          activeIndex = files.size() - 1;
588
        }
589
      }
590
    }
591
592
    if( files.isEmpty() ) {
593
      newEditor();
594
    }
595
    else {
596
      openEditors( files, activeIndex );
597
    }
598
  }
599
600
  public void persistPreferences() {
601
    final var allEditors = getTabs();
602
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
603
604
    for( final var tab : allEditors ) {
605
      final var fileEditor = (FileEditorTab) tab;
606
      final var filePath = fileEditor.getPath();
607
608
      if( filePath != null ) {
609
        fileNames.add( filePath.toString() );
610
      }
611
    }
612
613
    final var preferences = getPreferences();
614
    Utils.putPrefsStrings( preferences,
615
                           "file",
616
                           fileNames.toArray( new String[ 0 ] ) );
617
618
    final var activeEditor = getActiveFileEditor();
619
    final var filePath = activeEditor == null ? null : activeEditor.getPath();
620
621
    if( filePath == null ) {
622
      preferences.remove( "activeFile" );
623
    }
624
    else {
625
      preferences.put( "activeFile", filePath.toString() );
626
    }
627
  }
628
629
  private List<String> getExtensions( final String key ) {
630
    return getSettings().getStringSettingList( key );
631
  }
632
633
  private Notifier getNotifyService() {
634
    return sNotifier;
635
  }
636
637
  private Settings getSettings() {
638
    return SETTINGS;
639
  }
640
641
  protected Options getOptions() {
642
    return sOptions;
643
  }
644
645
  private Window getWindow() {
646
    return getScene().getWindow();
647
  }
648
649
  private Preferences getPreferences() {
650
    return getOptions().getState();
651
  }
652
}
1653
A src/main/java/com/keenwrite/FileType.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
/**
31
 * Represents different file type classifications. These are high-level mappings
32
 * that correspond to the list of glob patterns found within {@code
33
 * settings.properties}.
34
 */
35
public enum FileType {
36
37
  ALL( "all" ),
38
  RMARKDOWN( "rmarkdown" ),
39
  RXML( "rxml" ),
40
  SOURCE( "source" ),
41
  DEFINITION( "definition" ),
42
  XML( "xml" ),
43
  CSV( "csv" ),
44
  JSON( "json" ),
45
  TOML( "toml" ),
46
  YAML( "yaml" ),
47
  PROPERTIES( "properties" ),
48
  UNKNOWN( "unknown" );
49
50
  private final String mType;
51
52
  /**
53
   * Default constructor for enumerated file type.
54
   *
55
   * @param type Human-readable name for the file type.
56
   */
57
  FileType( final String type ) {
58
    mType = type;
59
  }
60
61
  /**
62
   * Returns the file type that corresponds to the given string.
63
   *
64
   * @param type The string to compare against this enumeration of file types.
65
   * @return The corresponding File Type for the given string.
66
   * @throws IllegalArgumentException Type not found.
67
   */
68
  public static FileType from( final String type ) {
69
    for( final FileType fileType : FileType.values() ) {
70
      if( fileType.isType( type ) ) {
71
        return fileType;
72
      }
73
    }
74
75
    throw new IllegalArgumentException( type );
76
  }
77
78
  /**
79
   * Answers whether this file type matches the given string, case insensitive
80
   * comparison.
81
   *
82
   * @param type Presumably a file name extension to check against.
83
   * @return true The given extension corresponds to this enumerated type.
84
   */
85
  public boolean isType( final String type ) {
86
    return getType().equalsIgnoreCase( type );
87
  }
88
89
  /**
90
   * Returns the human-readable name for the file type.
91
   *
92
   * @return A non-null instance.
93
   */
94
  private String getType() {
95
    return mType;
96
  }
97
98
  /**
99
   * Returns the lowercase version of the file name extension.
100
   *
101
   * @return The file name, in lower case.
102
   */
103
  @Override
104
  public String toString() {
105
    return getType();
106
  }
107
}
1108
A src/main/java/com/keenwrite/Launcher.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import java.io.IOException;
31
import java.io.InputStream;
32
import java.util.Calendar;
33
import java.util.Properties;
34
35
import static com.keenwrite.Bootstrap.APP_TITLE;
36
import static java.lang.String.format;
37
38
/**
39
 * Launches the application using the {@link Main} class.
40
 *
41
 * <p>
42
 * This is required until modules are implemented, which may never happen
43
 * because the application should be ported away from Java and JavaFX.
44
 * </p>
45
 */
46
public class Launcher {
47
  /**
48
   * Delegates to the application entry point.
49
   *
50
   * @param args Command-line arguments.
51
   */
52
  public static void main( final String[] args ) throws IOException {
53
    showAppInfo();
54
    Main.main( args );
55
  }
56
57
  @SuppressWarnings("RedundantStringFormatCall")
58
  private static void showAppInfo() throws IOException {
59
    out( format( "%s version %s", APP_TITLE, getVersion() ) );
60
    out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) );
61
    out( format( "Portions copyright 2020 Karl Tauber." ) );
62
  }
63
64
  private static void out( final String s ) {
65
    System.out.println( s );
66
  }
67
68
  private static String getVersion() throws IOException {
69
    final Properties properties = loadProperties( "app.properties" );
70
    return properties.getProperty( "application.version" );
71
  }
72
73
  private static String getYear() {
74
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
75
  }
76
77
  @SuppressWarnings("SameParameterValue")
78
  private static Properties loadProperties( final String resource )
79
      throws IOException {
80
    final Properties properties = new Properties();
81
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
82
    return properties;
83
  }
84
85
  private static String getResourceName( final String resource ) {
86
    return format( "%s/%s", getPackagePath(), resource );
87
  }
88
89
  private static String getPackagePath() {
90
    return Launcher.class.getPackageName().replace( '.', '/' );
91
  }
92
93
  private static InputStream getResourceAsStream( final String resource ) {
94
    return Launcher.class.getClassLoader().getResourceAsStream( resource );
95
  }
96
}
197
A src/main/java/com/keenwrite/Main.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.keenwrite.preferences.FilePreferencesFactory;
31
import com.keenwrite.service.Options;
32
import com.keenwrite.service.Snitch;
33
import com.keenwrite.util.ResourceWalker;
34
import com.keenwrite.util.StageState;
35
import javafx.application.Application;
36
import javafx.scene.Scene;
37
import javafx.scene.image.Image;
38
import javafx.stage.Stage;
39
40
import java.awt.*;
41
import java.io.FileInputStream;
42
import java.io.IOException;
43
import java.io.InputStream;
44
import java.net.URI;
45
import java.util.Map;
46
import java.util.logging.LogManager;
47
48
import static com.keenwrite.Bootstrap.APP_TITLE;
49
import static com.keenwrite.Constants.*;
50
import static com.keenwrite.StatusBarNotifier.alert;
51
import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment;
52
import static java.awt.font.TextAttribute.*;
53
import static javafx.scene.input.KeyCode.F11;
54
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
55
56
/**
57
 * Application entry point. The application allows users to edit Markdown
58
 * files and see a real-time preview of the edits.
59
 */
60
public final class Main extends Application {
61
62
  static {
63
    // Suppress logging to standard output.
64
    LogManager.getLogManager().reset();
65
66
    // Suppress logging to standard error.
67
    System.err.close();
68
  }
69
70
  private final Options mOptions = Services.load( Options.class );
71
  private final Snitch mSnitch = Services.load( Snitch.class );
72
73
  private final Thread mSnitchThread = new Thread( getSnitch() );
74
  private final MainWindow mMainWindow = new MainWindow();
75
76
  @SuppressWarnings({"FieldCanBeLocal"})
77
  private StageState mStageState;
78
79
  /**
80
   * Application entry point.
81
   *
82
   * @param args Command-line arguments.
83
   */
84
  public static void main( final String[] args ) {
85
    initPreferences();
86
    initFonts();
87
    launch( args );
88
  }
89
90
  /**
91
   * JavaFX entry point.
92
   *
93
   * @param stage The primary application stage.
94
   */
95
  @Override
96
  public void start( final Stage stage ) {
97
    initState( stage );
98
    initStage( stage );
99
    initSnitch();
100
101
    stage.show();
102
103
    // After the stage is visible, the panel dimensions are
104
    // known, which allows scaling images to fit the preview panel.
105
    getMainWindow().init();
106
  }
107
108
  /**
109
   * This needs to run before the windowing system kicks in, otherwise the
110
   * fonts will not be found.
111
   */
112
  @SuppressWarnings({"rawtypes", "unchecked"})
113
  public static void initFonts() {
114
    final var ge = getLocalGraphicsEnvironment();
115
116
    try {
117
      ResourceWalker.walk(
118
          FONT_DIRECTORY, path -> {
119
            final var uri = path.toUri();
120
            final var filename = path.toString();
121
122
            try( final var is = openFont( uri, filename ) ) {
123
              final var font = Font.createFont( Font.TRUETYPE_FONT, is );
124
              final Map attributes = font.getAttributes();
125
126
              attributes.put( LIGATURES, LIGATURES_ON );
127
              attributes.put( KERNING, KERNING_ON );
128
              ge.registerFont( font.deriveFont( attributes ) );
129
            } catch( final Exception e ) {
130
              alert( e );
131
            }
132
          }
133
      );
134
    } catch( final Exception e ) {
135
      alert( e );
136
    }
137
  }
138
139
  private static InputStream openFont( final URI uri, final String filename )
140
      throws IOException {
141
    return uri.getScheme().equals( "jar" )
142
        ? Main.class.getResourceAsStream( filename )
143
        : new FileInputStream( filename );
144
  }
145
146
  /**
147
   * Sets the factory used for reading user preferences.
148
   */
149
  private static void initPreferences() {
150
    System.setProperty(
151
        "java.util.prefs.PreferencesFactory",
152
        FilePreferencesFactory.class.getName()
153
    );
154
  }
155
156
  private void initState( final Stage stage ) {
157
    mStageState = new StageState( stage, getOptions().getState() );
158
  }
159
160
  private void initStage( final Stage stage ) {
161
    stage.getIcons().addAll(
162
        createImage( FILE_LOGO_16 ),
163
        createImage( FILE_LOGO_32 ),
164
        createImage( FILE_LOGO_128 ),
165
        createImage( FILE_LOGO_256 ),
166
        createImage( FILE_LOGO_512 ) );
167
    stage.setTitle( APP_TITLE );
168
    stage.setScene( getScene() );
169
170
    stage.addEventHandler( KEY_PRESSED, event -> {
171
      if( F11.equals( event.getCode() ) ) {
172
        stage.setFullScreen( !stage.isFullScreen() );
173
      }
174
    } );
175
  }
176
177
  /**
178
   * Watch for file system changes.
179
   */
180
  private void initSnitch() {
181
    getSnitchThread().start();
182
  }
183
184
  /**
185
   * Stops the snitch service, if its running.
186
   *
187
   * @throws InterruptedException Couldn't stop the snitch thread.
188
   */
189
  @Override
190
  public void stop() throws InterruptedException {
191
    getSnitch().stop();
192
193
    final Thread thread = getSnitchThread();
194
    thread.interrupt();
195
    thread.join();
196
  }
197
198
  private Snitch getSnitch() {
199
    return mSnitch;
200
  }
201
202
  private Thread getSnitchThread() {
203
    return mSnitchThread;
204
  }
205
206
  private Options getOptions() {
207
    return mOptions;
208
  }
209
210
  private MainWindow getMainWindow() {
211
    return mMainWindow;
212
  }
213
214
  private Scene getScene() {
215
    return getMainWindow().getScene();
216
  }
217
218
  private Image createImage( final String filename ) {
219
    return new Image( filename );
220
  }
221
222
  /**
223
   * This is here to suppress an IDE warning, the method is not used.
224
   */
225
  public StageState getStageState() {
226
    return mStageState;
227
  }
228
}
1229
A src/main/java/com/keenwrite/MainWindow.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.dlsc.preferencesfx.PreferencesFxEvent;
31
import com.keenwrite.definition.DefinitionFactory;
32
import com.keenwrite.definition.DefinitionPane;
33
import com.keenwrite.definition.DefinitionSource;
34
import com.keenwrite.definition.MapInterpolator;
35
import com.keenwrite.definition.yaml.YamlDefinitionSource;
36
import com.keenwrite.editors.DefinitionNameInjector;
37
import com.keenwrite.editors.EditorPane;
38
import com.keenwrite.editors.markdown.MarkdownEditorPane;
39
import com.keenwrite.preferences.UserPreferences;
40
import com.keenwrite.preview.HTMLPreviewPane;
41
import com.keenwrite.exceptions.MissingFileException;
42
import com.keenwrite.processors.HtmlPreviewProcessor;
43
import com.keenwrite.processors.Processor;
44
import com.keenwrite.processors.ProcessorFactory;
45
import com.keenwrite.service.Options;
46
import com.keenwrite.service.Snitch;
47
import com.keenwrite.spelling.api.SpellCheckListener;
48
import com.keenwrite.spelling.api.SpellChecker;
49
import com.keenwrite.spelling.impl.PermissiveSpeller;
50
import com.keenwrite.spelling.impl.SymSpellSpeller;
51
import com.keenwrite.util.Action;
52
import com.keenwrite.util.ActionBuilder;
53
import com.keenwrite.util.ActionUtils;
54
import com.vladsch.flexmark.parser.Parser;
55
import com.vladsch.flexmark.util.ast.NodeVisitor;
56
import com.vladsch.flexmark.util.ast.VisitHandler;
57
import javafx.beans.binding.Bindings;
58
import javafx.beans.binding.BooleanBinding;
59
import javafx.beans.property.BooleanProperty;
60
import javafx.beans.property.SimpleBooleanProperty;
61
import javafx.beans.value.ChangeListener;
62
import javafx.beans.value.ObservableBooleanValue;
63
import javafx.beans.value.ObservableValue;
64
import javafx.collections.ListChangeListener.Change;
65
import javafx.collections.ObservableList;
66
import javafx.event.Event;
67
import javafx.event.EventHandler;
68
import javafx.geometry.Pos;
69
import javafx.scene.Node;
70
import javafx.scene.Scene;
71
import javafx.scene.control.*;
72
import javafx.scene.image.ImageView;
73
import javafx.scene.input.Clipboard;
74
import javafx.scene.input.ClipboardContent;
75
import javafx.scene.input.KeyEvent;
76
import javafx.scene.layout.BorderPane;
77
import javafx.scene.layout.VBox;
78
import javafx.scene.text.Text;
79
import javafx.stage.Window;
80
import javafx.stage.WindowEvent;
81
import javafx.util.Duration;
82
import org.apache.commons.lang3.SystemUtils;
83
import org.controlsfx.control.StatusBar;
84
import org.fxmisc.richtext.StyleClassedTextArea;
85
import org.fxmisc.richtext.model.StyleSpansBuilder;
86
import org.reactfx.value.Val;
87
88
import java.io.BufferedReader;
89
import java.io.InputStreamReader;
90
import java.nio.file.Path;
91
import java.util.*;
92
import java.util.concurrent.atomic.AtomicInteger;
93
import java.util.function.Consumer;
94
import java.util.function.Function;
95
import java.util.prefs.Preferences;
96
import java.util.stream.Collectors;
97
98
import static com.keenwrite.Bootstrap.APP_TITLE;
99
import static com.keenwrite.Constants.*;
100
import static com.keenwrite.Messages.get;
101
import static com.keenwrite.StatusBarNotifier.alert;
102
import static com.keenwrite.util.StageState.*;
103
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
104
import static java.nio.charset.StandardCharsets.UTF_8;
105
import static java.util.Collections.emptyList;
106
import static java.util.Collections.singleton;
107
import static javafx.application.Platform.runLater;
108
import static javafx.event.Event.fireEvent;
109
import static javafx.scene.control.Alert.AlertType.INFORMATION;
110
import static javafx.scene.input.KeyCode.ENTER;
111
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
112
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
113
114
/**
115
 * Main window containing a tab pane in the center for file editors.
116
 */
117
public class MainWindow implements Observer {
118
  /**
119
   * The {@code OPTIONS} variable must be declared before all other variables
120
   * to prevent subsequent initializations from failing due to missing user
121
   * preferences.
122
   */
123
  private static final Options sOptions = Services.load( Options.class );
124
  private static final Snitch SNITCH = Services.load( Snitch.class );
125
126
  private final Scene mScene;
127
  private final StatusBar mStatusBar;
128
  private final Text mLineNumberText;
129
  private final TextField mFindTextField;
130
  private final SpellChecker mSpellChecker;
131
132
  private final Object mMutex = new Object();
133
134
  /**
135
   * Prevents re-instantiation of processing classes.
136
   */
137
  private final Map<FileEditorTab, Processor<String>> mProcessors =
138
      new HashMap<>();
139
140
  private final Map<String, String> mResolvedMap =
141
      new HashMap<>( DEFAULT_MAP_SIZE );
142
143
  private final EventHandler<PreferencesFxEvent> mRPreferencesListener =
144
      event -> rerender();
145
146
  /**
147
   * Called when the definition data is changed.
148
   */
149
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
150
      mTreeHandler = event -> {
151
    exportDefinitions( getDefinitionPath() );
152
    interpolateResolvedMap();
153
    rerender();
154
  };
155
156
  /**
157
   * Called to inject the selected item when the user presses ENTER in the
158
   * definition pane.
159
   */
160
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
161
      event -> {
162
        if( event.getCode() == ENTER ) {
163
          getDefinitionNameInjector().injectSelectedItem();
164
        }
165
      };
166
167
  private final ChangeListener<Integer> mCaretPositionListener =
168
      ( observable, oldPosition, newPosition ) -> {
169
        final FileEditorTab tab = getActiveFileEditorTab();
170
        final EditorPane pane = tab.getEditorPane();
171
        final StyleClassedTextArea editor = pane.getEditor();
172
173
        getLineNumberText().setText(
174
            get( STATUS_BAR_LINE,
175
                 editor.getCurrentParagraph() + 1,
176
                 editor.getParagraphs().size(),
177
                 editor.getCaretPosition()
178
            )
179
        );
180
      };
181
182
  private final ChangeListener<Integer> mCaretParagraphListener =
183
      ( observable, oldIndex, newIndex ) ->
184
          scrollToParagraph( newIndex, true );
185
186
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
187
  private final DefinitionPane mDefinitionPane = createDefinitionPane();
188
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
189
  private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
190
      mCaretPositionListener,
191
      mCaretParagraphListener );
192
193
  /**
194
   * Listens on the definition pane for double-click events.
195
   */
196
  private final DefinitionNameInjector mDefinitionNameInjector
197
      = new DefinitionNameInjector( mDefinitionPane );
198
199
  public MainWindow() {
200
    mStatusBar = createStatusBar();
201
    mLineNumberText = createLineNumberText();
202
    mFindTextField = createFindTextField();
203
    mScene = createScene();
204
    mSpellChecker = createSpellChecker();
205
206
    // Add the close request listener before the window is shown.
207
    initLayout();
208
    StatusBarNotifier.setStatusBar( mStatusBar );
209
  }
210
211
  /**
212
   * Called after the stage is shown.
213
   */
214
  public void init() {
215
    initFindInput();
216
    initSnitch();
217
    initDefinitionListener();
218
    initTabAddedListener();
219
    initTabChangedListener();
220
    initPreferences();
221
    initVariableNameInjector();
222
  }
223
224
  private void initLayout() {
225
    final var scene = getScene();
226
227
    scene.getStylesheets().add( STYLESHEET_SCENE );
228
    scene.windowProperty().addListener(
229
        ( unused, oldWindow, newWindow ) ->
230
            newWindow.setOnCloseRequest(
231
                e -> {
232
                  if( !getFileEditorPane().closeAllEditors() ) {
233
                    e.consume();
234
                  }
235
                }
236
            )
237
    );
238
  }
239
240
  /**
241
   * Initialize the find input text field to listen on F3, ENTER, and
242
   * ESCAPE key presses.
243
   */
244
  private void initFindInput() {
245
    final TextField input = getFindTextField();
246
247
    input.setOnKeyPressed( ( KeyEvent event ) -> {
248
      switch( event.getCode() ) {
249
        case F3:
250
        case ENTER:
251
          editFindNext();
252
          break;
253
        case F:
254
          if( !event.isControlDown() ) {
255
            break;
256
          }
257
        case ESCAPE:
258
          getStatusBar().setGraphic( null );
259
          getActiveFileEditorTab().getEditorPane().requestFocus();
260
          break;
261
      }
262
    } );
263
264
    // Remove when the input field loses focus.
265
    input.focusedProperty().addListener(
266
        ( focused, oldFocus, newFocus ) -> {
267
          if( !newFocus ) {
268
            getStatusBar().setGraphic( null );
269
          }
270
        }
271
    );
272
  }
273
274
  /**
275
   * Watch for changes to external files. In particular, this awaits
276
   * modifications to any XSL files associated with XML files being edited.
277
   * When
278
   * an XSL file is modified (external to the application), the snitch's ears
279
   * perk up and the file is reloaded. This keeps the XSL transformation up to
280
   * date with what's on the file system.
281
   */
282
  private void initSnitch() {
283
    SNITCH.addObserver( this );
284
  }
285
286
  /**
287
   * Listen for {@link FileEditorTabPane} to receive open definition file
288
   * event.
289
   */
290
  private void initDefinitionListener() {
291
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
292
        ( final ObservableValue<? extends Path> file,
293
          final Path oldPath, final Path newPath ) -> {
294
          openDefinitions( newPath );
295
          rerender();
296
        }
297
    );
298
  }
299
300
  /**
301
   * Re-instantiates all processors then re-renders the active tab. This
302
   * will refresh the resolved map, force R to re-initialize, and brute-force
303
   * XSLT file reloads.
304
   */
305
  private void rerender() {
306
    runLater(
307
        () -> {
308
          resetProcessors();
309
          renderActiveTab();
310
        }
311
    );
312
  }
313
314
  /**
315
   * When tabs are added, hook the various change listeners onto the new
316
   * tab sothat the preview pane refreshes as necessary.
317
   */
318
  private void initTabAddedListener() {
319
    final FileEditorTabPane editorPane = getFileEditorPane();
320
321
    // Make sure the text processor kicks off when new files are opened.
322
    final ObservableList<Tab> tabs = editorPane.getTabs();
323
324
    // Update the preview pane on tab changes.
325
    tabs.addListener(
326
        ( final Change<? extends Tab> change ) -> {
327
          while( change.next() ) {
328
            if( change.wasAdded() ) {
329
              // Multiple tabs can be added simultaneously.
330
              for( final Tab newTab : change.getAddedSubList() ) {
331
                final FileEditorTab tab = (FileEditorTab) newTab;
332
333
                initTextChangeListener( tab );
334
                initScrollEventListener( tab );
335
                initSpellCheckListener( tab );
336
//              initSyntaxListener( tab );
337
              }
338
            }
339
          }
340
        }
341
    );
342
  }
343
344
  private void initTextChangeListener( final FileEditorTab tab ) {
345
    tab.addTextChangeListener(
346
        ( __, ov, nv ) -> {
347
          process( tab );
348
          scrollToParagraph( getCurrentParagraphIndex() );
349
        }
350
    );
351
  }
352
353
  private void initScrollEventListener( final FileEditorTab tab ) {
354
    final var scrollPane = tab.getScrollPane();
355
    final var scrollBar = getPreviewPane().getVerticalScrollBar();
356
357
    addShowListener( scrollPane, ( __ ) -> {
358
      final var handler = new ScrollEventHandler( scrollPane, scrollBar );
359
      handler.enabledProperty().bind( tab.selectedProperty() );
360
    } );
361
  }
362
363
  /**
364
   * Listen for changes to the any particular paragraph and perform a quick
365
   * spell check upon it. The style classes in the editor will be changed to
366
   * mark any spelling mistakes in the paragraph. The user may then interact
367
   * with any misspelled word (i.e., any piece of text that is marked) to
368
   * revise the spelling.
369
   *
370
   * @param tab The tab to spellcheck.
371
   */
372
  private void initSpellCheckListener( final FileEditorTab tab ) {
373
    final var editor = tab.getEditorPane().getEditor();
374
375
    // When the editor first appears, run a full spell check. This allows
376
    // spell checking while typing to be restricted to the active paragraph,
377
    // which is usually substantially smaller than the whole document.
378
    addShowListener(
379
        editor, ( __ ) -> spellcheck( editor, editor.getText() )
380
    );
381
382
    // Use the plain text changes so that notifications of style changes
383
    // are suppressed. Checking against the identity ensures that only
384
    // new text additions or deletions trigger proofreading.
385
    editor.plainTextChanges()
386
          .filter( p -> !p.isIdentity() ).subscribe( change -> {
387
388
      // Only perform a spell check on the current paragraph. The
389
      // entire document is processed once, when opened.
390
      final var offset = change.getPosition();
391
      final var position = editor.offsetToPosition( offset, Forward );
392
      final var paraId = position.getMajor();
393
      final var paragraph = editor.getParagraph( paraId );
394
      final var text = paragraph.getText();
395
396
      // Ensure that styles aren't doubled-up.
397
      editor.clearStyle( paraId );
398
399
      spellcheck( editor, text, paraId );
400
    } );
401
  }
402
403
  /**
404
   * Listen for new tab selection events.
405
   */
406
  private void initTabChangedListener() {
407
    final FileEditorTabPane editorPane = getFileEditorPane();
408
409
    // Update the preview pane changing tabs.
410
    editorPane.addTabSelectionListener(
411
        ( tabPane, oldTab, newTab ) -> {
412
          if( newTab == null ) {
413
            // Clear the preview pane when closing an editor. When the last
414
            // tab is closed, this ensures that the preview pane is empty.
415
            getPreviewPane().clear();
416
          }
417
          else {
418
            final var tab = (FileEditorTab) newTab;
419
            updateVariableNameInjector( tab );
420
            process( tab );
421
          }
422
        }
423
    );
424
  }
425
426
  /**
427
   * Reloads the preferences from the previous session.
428
   */
429
  private void initPreferences() {
430
    initDefinitionPane();
431
    getFileEditorPane().initPreferences();
432
    getUserPreferences().addSaveEventHandler( mRPreferencesListener );
433
  }
434
435
  private void initVariableNameInjector() {
436
    updateVariableNameInjector( getActiveFileEditorTab() );
437
  }
438
439
  /**
440
   * Calls the listener when the given node is shown for the first time. The
441
   * visible property is not the same as the initial showing event; visibility
442
   * can be triggered numerous times (such as going off screen).
443
   * <p>
444
   * This is called, for example, before the drag handler can be attached,
445
   * because the scrollbar for the text editor pane must be visible.
446
   * </p>
447
   *
448
   * @param node     The node to watch for showing.
449
   * @param consumer The consumer to invoke when the event fires.
450
   */
451
  private void addShowListener(
452
      final Node node, final Consumer<Void> consumer ) {
453
    final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
454
        runLater( () -> {
455
          if( newShow != null && newShow ) {
456
            try {
457
              consumer.accept( null );
458
            } catch( final Exception ex ) {
459
              alert( ex );
460
            }
461
          }
462
        } );
463
464
    Val.flatMap( node.sceneProperty(), Scene::windowProperty )
465
       .flatMap( Window::showingProperty )
466
       .addListener( listener );
467
  }
468
469
  private void scrollToParagraph( final int id ) {
470
    scrollToParagraph( id, false );
471
  }
472
473
  /**
474
   * @param id    The paragraph to scroll to, will be approximated if it doesn't
475
   *              exist.
476
   * @param force {@code true} means to force scrolling immediately, which
477
   *              should only be attempted when it is known that the document
478
   *              has been fully rendered. Otherwise the internal map of ID
479
   *              attributes will be incomplete and scrolling will flounder.
480
   */
481
  private void scrollToParagraph( final int id, final boolean force ) {
482
    synchronized( mMutex ) {
483
      final var previewPane = getPreviewPane();
484
      final var scrollPane = previewPane.getScrollPane();
485
      final int approxId = getActiveEditorPane().approximateParagraphId( id );
486
487
      if( force ) {
488
        previewPane.scrollTo( approxId );
489
      }
490
      else {
491
        previewPane.tryScrollTo( approxId );
492
      }
493
494
      scrollPane.repaint();
495
    }
496
  }
497
498
  private void updateVariableNameInjector( final FileEditorTab tab ) {
499
    getDefinitionNameInjector().addListener( tab );
500
  }
501
502
  /**
503
   * Called whenever the preview pane becomes out of sync with the file editor
504
   * tab. This can be called when the text changes, the caret paragraph
505
   * changes, or the file tab changes.
506
   *
507
   * @param tab The file editor tab that has been changed in some fashion.
508
   */
509
  private void process( final FileEditorTab tab ) {
510
    if( tab != null ) {
511
      getPreviewPane().setPath( tab.getPath() );
512
513
      final Processor<String> processor = getProcessors().computeIfAbsent(
514
          tab, p -> createProcessors( tab )
515
      );
516
517
      try {
518
        processChain( processor, tab.getEditorText() );
519
      } catch( final Exception ex ) {
520
        alert( ex );
521
      }
522
    }
523
  }
524
525
  /**
526
   * Executes the processing chain, operating on the given string.
527
   *
528
   * @param handler The first processor in the chain to call.
529
   * @param text    The initial value of the text to process.
530
   * @return The final value of the text that was processed by the chain.
531
   */
532
  private String processChain( Processor<String> handler, String text ) {
533
    while( handler != null && text != null ) {
534
      text = handler.apply( text );
535
      handler = handler.next();
536
    }
537
538
    return text;
539
  }
540
541
  private void renderActiveTab() {
542
    process( getActiveFileEditorTab() );
543
  }
544
545
  /**
546
   * Called when a definition source is opened.
547
   *
548
   * @param path Path to the definition source that was opened.
549
   */
550
  private void openDefinitions( final Path path ) {
551
    try {
552
      final var ds = createDefinitionSource( path );
553
      setDefinitionSource( ds );
554
555
      final var prefs = getUserPreferences();
556
      prefs.definitionPathProperty().setValue( path.toFile() );
557
      prefs.save();
558
559
      final var tooltipPath = new Tooltip( path.toString() );
560
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
561
562
      final var pane = getDefinitionPane();
563
      pane.update( ds );
564
      pane.addTreeChangeHandler( mTreeHandler );
565
      pane.addKeyEventHandler( mDefinitionKeyHandler );
566
      pane.filenameProperty().setValue( path.getFileName().toString() );
567
      pane.setTooltip( tooltipPath );
568
569
      interpolateResolvedMap();
570
    } catch( final Exception ex ) {
571
      alert( ex );
572
    }
573
  }
574
575
  private void exportDefinitions( final Path path ) {
576
    try {
577
      final var pane = getDefinitionPane();
578
      final var root = pane.getTreeView().getRoot();
579
      final var problemChild = pane.isTreeWellFormed();
580
581
      if( problemChild == null ) {
582
        getDefinitionSource().getTreeAdapter().export( root, path );
583
      }
584
      else {
585
        alert( "yaml.error.tree.form", problemChild.getValue() );
586
      }
587
    } catch( final Exception ex ) {
588
      alert( ex );
589
    }
590
  }
591
592
  private void interpolateResolvedMap() {
593
    final var treeMap = getDefinitionPane().toMap();
594
    final var map = new HashMap<>( treeMap );
595
    MapInterpolator.interpolate( map );
596
597
    getResolvedMap().clear();
598
    getResolvedMap().putAll( map );
599
  }
600
601
  private void initDefinitionPane() {
602
    openDefinitions( getDefinitionPath() );
603
  }
604
605
  //---- File actions -------------------------------------------------------
606
607
  /**
608
   * Called when an {@link Observable} instance has changed. This is called
609
   * by both the {@link Snitch} service and the notify service. The @link
610
   * Snitch} service can be called for different file types, including
611
   * {@link DefinitionSource} instances.
612
   *
613
   * @param observable The observed instance.
614
   * @param value      The noteworthy item.
615
   */
616
  @Override
617
  public void update( final Observable observable, final Object value ) {
618
    if( value instanceof Path && observable instanceof Snitch ) {
619
      updateSelectedTab();
620
    }
621
  }
622
623
  /**
624
   * Called when a file has been modified.
625
   */
626
  private void updateSelectedTab() {
627
    rerender();
628
  }
629
630
  /**
631
   * After resetting the processors, they will refresh anew to be up-to-date
632
   * with the files (text and definition) currently loaded into the editor.
633
   */
634
  private void resetProcessors() {
635
    getProcessors().clear();
636
  }
637
638
  //---- File actions -------------------------------------------------------
639
640
  private void fileNew() {
641
    getFileEditorPane().newEditor();
642
  }
643
644
  private void fileOpen() {
645
    getFileEditorPane().openFileDialog();
646
  }
647
648
  private void fileClose() {
649
    getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
650
  }
651
652
  /**
653
   * TODO: Upon closing, first remove the tab change listeners. (There's no
654
   * need to re-render each tab when all are being closed.)
655
   */
656
  private void fileCloseAll() {
657
    getFileEditorPane().closeAllEditors();
658
  }
659
660
  private void fileSave() {
661
    getFileEditorPane().saveEditor( getActiveFileEditorTab() );
662
  }
663
664
  private void fileSaveAs() {
665
    final FileEditorTab editor = getActiveFileEditorTab();
666
    getFileEditorPane().saveEditorAs( editor );
667
    getProcessors().remove( editor );
668
669
    try {
670
      process( editor );
671
    } catch( final Exception ex ) {
672
      alert( ex );
673
    }
674
  }
675
676
  private void fileSaveAll() {
677
    getFileEditorPane().saveAllEditors();
678
  }
679
680
  private void fileExit() {
681
    final Window window = getWindow();
682
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
683
  }
684
685
  //---- Edit actions -------------------------------------------------------
686
687
  /**
688
   * Transform the Markdown into HTML then copy that HTML into the copy
689
   * buffer.
690
   */
691
  private void copyHtml() {
692
    final var markdown = getActiveEditorPane().getText();
693
    final var processors = createProcessorFactory().createProcessors(
694
        getActiveFileEditorTab()
695
    );
696
697
    final var chain = processors.remove( HtmlPreviewProcessor.class );
698
699
    final String html = processChain( chain, markdown );
700
701
    final Clipboard clipboard = Clipboard.getSystemClipboard();
702
    final ClipboardContent content = new ClipboardContent();
703
    content.putString( html );
704
    clipboard.setContent( content );
705
  }
706
707
  /**
708
   * Used to find text in the active file editor window.
709
   */
710
  private void editFind() {
711
    final TextField input = getFindTextField();
712
    getStatusBar().setGraphic( input );
713
    input.requestFocus();
714
  }
715
716
  public void editFindNext() {
717
    getActiveFileEditorTab().searchNext( getFindTextField().getText() );
718
  }
719
720
  public void editPreferences() {
721
    getUserPreferences().show();
722
  }
723
724
  //---- Insert actions -----------------------------------------------------
725
726
  /**
727
   * Delegates to the active editor to handle wrapping the current text
728
   * selection with leading and trailing strings.
729
   *
730
   * @param leading  The string to put before the selection.
731
   * @param trailing The string to put after the selection.
732
   */
733
  private void insertMarkdown(
734
      final String leading, final String trailing ) {
735
    getActiveEditorPane().surroundSelection( leading, trailing );
736
  }
737
738
  private void insertMarkdown(
739
      final String leading, final String trailing, final String hint ) {
740
    getActiveEditorPane().surroundSelection( leading, trailing, hint );
741
  }
742
743
  //---- View actions -------------------------------------------------------
744
745
  private void viewRefresh() {
746
    rerender();
747
  }
748
749
  //---- Help actions -------------------------------------------------------
750
751
  private void helpAbout() {
752
    final Alert alert = new Alert( INFORMATION );
753
    alert.setTitle( get( "Dialog.about.title", APP_TITLE ) );
754
    alert.setHeaderText( get( "Dialog.about.header", APP_TITLE ) );
755
    alert.setContentText( get( "Dialog.about.content" ) );
756
    alert.setGraphic( new ImageView( ICON_DIALOG ) );
757
    alert.initOwner( getWindow() );
758
759
    alert.showAndWait();
760
  }
761
762
  //---- Member creators ----------------------------------------------------
763
764
  private SpellChecker createSpellChecker() {
765
    try {
766
      final Collection<String> lexicon = readLexicon( "en.txt" );
767
      return SymSpellSpeller.forLexicon( lexicon );
768
    } catch( final Exception ex ) {
769
      alert( ex );
770
      return new PermissiveSpeller();
771
    }
772
  }
773
774
  /**
775
   * Factory to create processors that are suited to different file types.
776
   *
777
   * @param tab The tab that is subjected to processing.
778
   * @return A processor suited to the file type specified by the tab's path.
779
   */
780
  private Processor<String> createProcessors( final FileEditorTab tab ) {
781
    return createProcessorFactory().createProcessors( tab );
782
  }
783
784
  private ProcessorFactory createProcessorFactory() {
785
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
786
  }
787
788
  private DefinitionPane createDefinitionPane() {
789
    return new DefinitionPane();
790
  }
791
792
  private HTMLPreviewPane createHTMLPreviewPane() {
793
    return new HTMLPreviewPane();
794
  }
795
796
  private DefinitionSource createDefaultDefinitionSource() {
797
    return new YamlDefinitionSource( getDefinitionPath() );
798
  }
799
800
  private DefinitionSource createDefinitionSource( final Path path ) {
801
    try {
802
      return createDefinitionFactory().createDefinitionSource( path );
803
    } catch( final Exception ex ) {
804
      alert( ex );
805
      return createDefaultDefinitionSource();
806
    }
807
  }
808
809
  private TextField createFindTextField() {
810
    return new TextField();
811
  }
812
813
  private DefinitionFactory createDefinitionFactory() {
814
    return new DefinitionFactory();
815
  }
816
817
  private StatusBar createStatusBar() {
818
    return new StatusBar();
819
  }
820
821
  private Scene createScene() {
822
    final SplitPane splitPane = new SplitPane(
823
        getDefinitionPane(),
824
        getFileEditorPane(),
825
        getPreviewPane() );
826
827
    splitPane.setDividerPositions(
828
        getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
829
        getFloat( K_PANE_SPLIT_EDITOR, .60f ),
830
        getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
831
832
    getDefinitionPane().prefHeightProperty()
833
                       .bind( splitPane.heightProperty() );
834
835
    final BorderPane borderPane = new BorderPane();
836
    borderPane.setPrefSize( 1280, 800 );
837
    borderPane.setTop( createMenuBar() );
838
    borderPane.setBottom( getStatusBar() );
839
    borderPane.setCenter( splitPane );
840
841
    final VBox statusBar = new VBox();
842
    statusBar.setAlignment( Pos.BASELINE_CENTER );
843
    statusBar.getChildren().add( getLineNumberText() );
844
    getStatusBar().getRightItems().add( statusBar );
845
846
    // Force preview pane refresh on Windows.
847
    if( SystemUtils.IS_OS_WINDOWS ) {
848
      splitPane.getDividers().get( 1 ).positionProperty().addListener(
849
          ( l, oValue, nValue ) -> runLater(
850
              () -> getPreviewPane().getScrollPane().repaint()
851
          )
852
      );
853
    }
854
855
    return new Scene( borderPane );
856
  }
857
858
  private Text createLineNumberText() {
859
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
860
  }
861
862
  private Node createMenuBar() {
863
    final BooleanBinding activeFileEditorIsNull =
864
        getFileEditorPane().activeFileEditorProperty().isNull();
865
866
    // File actions
867
    final Action fileNewAction = new ActionBuilder()
868
        .setText( "Main.menu.file.new" )
869
        .setAccelerator( "Shortcut+N" )
870
        .setIcon( FILE_ALT )
871
        .setAction( e -> fileNew() )
872
        .build();
873
    final Action fileOpenAction = new ActionBuilder()
874
        .setText( "Main.menu.file.open" )
875
        .setAccelerator( "Shortcut+O" )
876
        .setIcon( FOLDER_OPEN_ALT )
877
        .setAction( e -> fileOpen() )
878
        .build();
879
    final Action fileCloseAction = new ActionBuilder()
880
        .setText( "Main.menu.file.close" )
881
        .setAccelerator( "Shortcut+W" )
882
        .setAction( e -> fileClose() )
883
        .setDisable( activeFileEditorIsNull )
884
        .build();
885
    final Action fileCloseAllAction = new ActionBuilder()
886
        .setText( "Main.menu.file.close_all" )
887
        .setAction( e -> fileCloseAll() )
888
        .setDisable( activeFileEditorIsNull )
889
        .build();
890
    final Action fileSaveAction = new ActionBuilder()
891
        .setText( "Main.menu.file.save" )
892
        .setAccelerator( "Shortcut+S" )
893
        .setIcon( FLOPPY_ALT )
894
        .setAction( e -> fileSave() )
895
        .setDisable( createActiveBooleanProperty(
896
            FileEditorTab::modifiedProperty ).not() )
897
        .build();
898
    final Action fileSaveAsAction = new ActionBuilder()
899
        .setText( "Main.menu.file.save_as" )
900
        .setAction( e -> fileSaveAs() )
901
        .setDisable( activeFileEditorIsNull )
902
        .build();
903
    final Action fileSaveAllAction = new ActionBuilder()
904
        .setText( "Main.menu.file.save_all" )
905
        .setAccelerator( "Shortcut+Shift+S" )
906
        .setAction( e -> fileSaveAll() )
907
        .setDisable( Bindings.not(
908
            getFileEditorPane().anyFileEditorModifiedProperty() ) )
909
        .build();
910
    final Action fileExitAction = new ActionBuilder()
911
        .setText( "Main.menu.file.exit" )
912
        .setAction( e -> fileExit() )
913
        .build();
914
915
    // Edit actions
916
    final Action editCopyHtmlAction = new ActionBuilder()
917
        .setText( "Main.menu.edit.copy.html" )
918
        .setIcon( HTML5 )
919
        .setAction( e -> copyHtml() )
920
        .setDisable( activeFileEditorIsNull )
921
        .build();
922
923
    final Action editUndoAction = new ActionBuilder()
924
        .setText( "Main.menu.edit.undo" )
925
        .setAccelerator( "Shortcut+Z" )
926
        .setIcon( UNDO )
927
        .setAction( e -> getActiveEditorPane().undo() )
928
        .setDisable( createActiveBooleanProperty(
929
            FileEditorTab::canUndoProperty ).not() )
930
        .build();
931
    final Action editRedoAction = new ActionBuilder()
932
        .setText( "Main.menu.edit.redo" )
933
        .setAccelerator( "Shortcut+Y" )
934
        .setIcon( REPEAT )
935
        .setAction( e -> getActiveEditorPane().redo() )
936
        .setDisable( createActiveBooleanProperty(
937
            FileEditorTab::canRedoProperty ).not() )
938
        .build();
939
940
    final Action editCutAction = new ActionBuilder()
941
        .setText( "Main.menu.edit.cut" )
942
        .setAccelerator( "Shortcut+X" )
943
        .setIcon( CUT )
944
        .setAction( e -> getActiveEditorPane().cut() )
945
        .setDisable( activeFileEditorIsNull )
946
        .build();
947
    final Action editCopyAction = new ActionBuilder()
948
        .setText( "Main.menu.edit.copy" )
949
        .setAccelerator( "Shortcut+C" )
950
        .setIcon( COPY )
951
        .setAction( e -> getActiveEditorPane().copy() )
952
        .setDisable( activeFileEditorIsNull )
953
        .build();
954
    final Action editPasteAction = new ActionBuilder()
955
        .setText( "Main.menu.edit.paste" )
956
        .setAccelerator( "Shortcut+V" )
957
        .setIcon( PASTE )
958
        .setAction( e -> getActiveEditorPane().paste() )
959
        .setDisable( activeFileEditorIsNull )
960
        .build();
961
    final Action editSelectAllAction = new ActionBuilder()
962
        .setText( "Main.menu.edit.selectAll" )
963
        .setAccelerator( "Shortcut+A" )
964
        .setAction( e -> getActiveEditorPane().selectAll() )
965
        .setDisable( activeFileEditorIsNull )
966
        .build();
967
968
    final Action editFindAction = new ActionBuilder()
969
        .setText( "Main.menu.edit.find" )
970
        .setAccelerator( "Ctrl+F" )
971
        .setIcon( SEARCH )
972
        .setAction( e -> editFind() )
973
        .setDisable( activeFileEditorIsNull )
974
        .build();
975
    final Action editFindNextAction = new ActionBuilder()
976
        .setText( "Main.menu.edit.find.next" )
977
        .setAccelerator( "F3" )
978
        .setIcon( null )
979
        .setAction( e -> editFindNext() )
980
        .setDisable( activeFileEditorIsNull )
981
        .build();
982
    final Action editPreferencesAction = new ActionBuilder()
983
        .setText( "Main.menu.edit.preferences" )
984
        .setAccelerator( "Ctrl+Alt+S" )
985
        .setAction( e -> editPreferences() )
986
        .build();
987
988
    // Format actions
989
    final Action formatBoldAction = new ActionBuilder()
990
        .setText( "Main.menu.format.bold" )
991
        .setAccelerator( "Shortcut+B" )
992
        .setIcon( BOLD )
993
        .setAction( e -> insertMarkdown( "**", "**" ) )
994
        .setDisable( activeFileEditorIsNull )
995
        .build();
996
    final Action formatItalicAction = new ActionBuilder()
997
        .setText( "Main.menu.format.italic" )
998
        .setAccelerator( "Shortcut+I" )
999
        .setIcon( ITALIC )
1000
        .setAction( e -> insertMarkdown( "*", "*" ) )
1001
        .setDisable( activeFileEditorIsNull )
1002
        .build();
1003
    final Action formatSuperscriptAction = new ActionBuilder()
1004
        .setText( "Main.menu.format.superscript" )
1005
        .setAccelerator( "Shortcut+[" )
1006
        .setIcon( SUPERSCRIPT )
1007
        .setAction( e -> insertMarkdown( "^", "^" ) )
1008
        .setDisable( activeFileEditorIsNull )
1009
        .build();
1010
    final Action formatSubscriptAction = new ActionBuilder()
1011
        .setText( "Main.menu.format.subscript" )
1012
        .setAccelerator( "Shortcut+]" )
1013
        .setIcon( SUBSCRIPT )
1014
        .setAction( e -> insertMarkdown( "~", "~" ) )
1015
        .setDisable( activeFileEditorIsNull )
1016
        .build();
1017
    final Action formatStrikethroughAction = new ActionBuilder()
1018
        .setText( "Main.menu.format.strikethrough" )
1019
        .setAccelerator( "Shortcut+T" )
1020
        .setIcon( STRIKETHROUGH )
1021
        .setAction( e -> insertMarkdown( "~~", "~~" ) )
1022
        .setDisable( activeFileEditorIsNull )
1023
        .build();
1024
1025
    // Insert actions
1026
    final Action insertBlockquoteAction = new ActionBuilder()
1027
        .setText( "Main.menu.insert.blockquote" )
1028
        .setAccelerator( "Ctrl+Q" )
1029
        .setIcon( QUOTE_LEFT )
1030
        .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
1031
        .setDisable( activeFileEditorIsNull )
1032
        .build();
1033
    final Action insertCodeAction = new ActionBuilder()
1034
        .setText( "Main.menu.insert.code" )
1035
        .setAccelerator( "Shortcut+K" )
1036
        .setIcon( CODE )
1037
        .setAction( e -> insertMarkdown( "`", "`" ) )
1038
        .setDisable( activeFileEditorIsNull )
1039
        .build();
1040
    final Action insertFencedCodeBlockAction = new ActionBuilder()
1041
        .setText( "Main.menu.insert.fenced_code_block" )
1042
        .setAccelerator( "Shortcut+Shift+K" )
1043
        .setIcon( FILE_CODE_ALT )
1044
        .setAction( e -> insertMarkdown(
1045
            "\n\n```\n",
1046
            "\n```\n\n",
1047
            get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
1048
        .setDisable( activeFileEditorIsNull )
1049
        .build();
1050
    final Action insertLinkAction = new ActionBuilder()
1051
        .setText( "Main.menu.insert.link" )
1052
        .setAccelerator( "Shortcut+L" )
1053
        .setIcon( LINK )
1054
        .setAction( e -> getActiveEditorPane().insertLink() )
1055
        .setDisable( activeFileEditorIsNull )
1056
        .build();
1057
    final Action insertImageAction = new ActionBuilder()
1058
        .setText( "Main.menu.insert.image" )
1059
        .setAccelerator( "Shortcut+G" )
1060
        .setIcon( PICTURE_ALT )
1061
        .setAction( e -> getActiveEditorPane().insertImage() )
1062
        .setDisable( activeFileEditorIsNull )
1063
        .build();
1064
1065
    // Number of heading actions (H1 ... H3)
1066
    final int HEADINGS = 3;
1067
    final Action[] headings = new Action[ HEADINGS ];
1068
1069
    for( int i = 1; i <= HEADINGS; i++ ) {
1070
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
1071
      final String markup = String.format( "%n%n%s ", hashes );
1072
      final String text = "Main.menu.insert.heading." + i;
1073
      final String accelerator = "Shortcut+" + i;
1074
      final String prompt = text + ".prompt";
1075
1076
      headings[ i - 1 ] = new ActionBuilder()
1077
          .setText( text )
1078
          .setAccelerator( accelerator )
1079
          .setIcon( HEADER )
1080
          .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
1081
          .setDisable( activeFileEditorIsNull )
1082
          .build();
1083
    }
1084
1085
    final Action insertUnorderedListAction = new ActionBuilder()
1086
        .setText( "Main.menu.insert.unordered_list" )
1087
        .setAccelerator( "Shortcut+U" )
1088
        .setIcon( LIST_UL )
1089
        .setAction( e -> insertMarkdown( "\n\n* ", "" ) )
1090
        .setDisable( activeFileEditorIsNull )
1091
        .build();
1092
    final Action insertOrderedListAction = new ActionBuilder()
1093
        .setText( "Main.menu.insert.ordered_list" )
1094
        .setAccelerator( "Shortcut+Shift+O" )
1095
        .setIcon( LIST_OL )
1096
        .setAction( e -> insertMarkdown(
1097
            "\n\n1. ", "" ) )
1098
        .setDisable( activeFileEditorIsNull )
1099
        .build();
1100
    final Action insertHorizontalRuleAction = new ActionBuilder()
1101
        .setText( "Main.menu.insert.horizontal_rule" )
1102
        .setAccelerator( "Shortcut+H" )
1103
        .setAction( e -> insertMarkdown(
1104
            "\n\n---\n\n", "" ) )
1105
        .setDisable( activeFileEditorIsNull )
1106
        .build();
1107
1108
    // Definition actions
1109
    final Action definitionCreateAction = new ActionBuilder()
1110
        .setText( "Main.menu.definition.create" )
1111
        .setIcon( TREE )
1112
        .setAction( e -> getDefinitionPane().addItem() )
1113
        .build();
1114
    final Action definitionInsertAction = new ActionBuilder()
1115
        .setText( "Main.menu.definition.insert" )
1116
        .setAccelerator( "Ctrl+Space" )
1117
        .setIcon( STAR )
1118
        .setAction( e -> definitionInsert() )
1119
        .build();
1120
1121
    // Help actions
1122
    final Action helpAboutAction = new ActionBuilder()
1123
        .setText( "Main.menu.help.about" )
1124
        .setAction( e -> helpAbout() )
1125
        .build();
1126
1127
    //---- MenuBar ----
1128
1129
    // File Menu
1130
    final var fileMenu = ActionUtils.createMenu(
1131
        get( "Main.menu.file" ),
1132
        fileNewAction,
1133
        fileOpenAction,
1134
        null,
1135
        fileCloseAction,
1136
        fileCloseAllAction,
1137
        null,
1138
        fileSaveAction,
1139
        fileSaveAsAction,
1140
        fileSaveAllAction,
1141
        null,
1142
        fileExitAction );
1143
1144
    // Edit Menu
1145
    final var editMenu = ActionUtils.createMenu(
1146
        get( "Main.menu.edit" ),
1147
        editCopyHtmlAction,
1148
        null,
1149
        editUndoAction,
1150
        editRedoAction,
1151
        null,
1152
        editCutAction,
1153
        editCopyAction,
1154
        editPasteAction,
1155
        editSelectAllAction,
1156
        null,
1157
        editFindAction,
1158
        editFindNextAction,
1159
        null,
1160
        editPreferencesAction );
1161
1162
    // Format Menu
1163
    final var formatMenu = ActionUtils.createMenu(
1164
        get( "Main.menu.format" ),
1165
        formatBoldAction,
1166
        formatItalicAction,
1167
        formatSuperscriptAction,
1168
        formatSubscriptAction,
1169
        formatStrikethroughAction
1170
    );
1171
1172
    // Insert Menu
1173
    final var insertMenu = ActionUtils.createMenu(
1174
        get( "Main.menu.insert" ),
1175
        insertBlockquoteAction,
1176
        insertCodeAction,
1177
        insertFencedCodeBlockAction,
1178
        null,
1179
        insertLinkAction,
1180
        insertImageAction,
1181
        null,
1182
        headings[ 0 ],
1183
        headings[ 1 ],
1184
        headings[ 2 ],
1185
        null,
1186
        insertUnorderedListAction,
1187
        insertOrderedListAction,
1188
        insertHorizontalRuleAction
1189
    );
1190
1191
    // Definition Menu
1192
    final var definitionMenu = ActionUtils.createMenu(
1193
        get( "Main.menu.definition" ),
1194
        definitionCreateAction,
1195
        definitionInsertAction );
1196
1197
    // Help Menu
1198
    final var helpMenu = ActionUtils.createMenu(
1199
        get( "Main.menu.help" ),
1200
        helpAboutAction );
1201
1202
    //---- MenuBar ----
1203
    final var menuBar = new MenuBar(
1204
        fileMenu,
1205
        editMenu,
1206
        formatMenu,
1207
        insertMenu,
1208
        definitionMenu,
1209
        helpMenu );
1210
1211
    //---- ToolBar ----
1212
    final var toolBar = ActionUtils.createToolBar(
1213
        fileNewAction,
1214
        fileOpenAction,
1215
        fileSaveAction,
1216
        null,
1217
        editUndoAction,
1218
        editRedoAction,
1219
        editCutAction,
1220
        editCopyAction,
1221
        editPasteAction,
1222
        null,
1223
        formatBoldAction,
1224
        formatItalicAction,
1225
        formatSuperscriptAction,
1226
        formatSubscriptAction,
1227
        insertBlockquoteAction,
1228
        insertCodeAction,
1229
        insertFencedCodeBlockAction,
1230
        null,
1231
        insertLinkAction,
1232
        insertImageAction,
1233
        null,
1234
        headings[ 0 ],
1235
        null,
1236
        insertUnorderedListAction,
1237
        insertOrderedListAction );
1238
1239
    return new VBox( menuBar, toolBar );
1240
  }
1241
1242
  /**
1243
   * Performs the autoinsert function on the active file editor.
1244
   */
1245
  private void definitionInsert() {
1246
    getDefinitionNameInjector().autoinsert();
1247
  }
1248
1249
  /**
1250
   * Creates a boolean property that is bound to another boolean value of the
1251
   * active editor.
1252
   */
1253
  private BooleanProperty createActiveBooleanProperty(
1254
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1255
1256
    final BooleanProperty b = new SimpleBooleanProperty();
1257
    final FileEditorTab tab = getActiveFileEditorTab();
1258
1259
    if( tab != null ) {
1260
      b.bind( func.apply( tab ) );
1261
    }
1262
1263
    getFileEditorPane().activeFileEditorProperty().addListener(
1264
        ( observable, oldFileEditor, newFileEditor ) -> {
1265
          b.unbind();
1266
1267
          if( newFileEditor == null ) {
1268
            b.set( false );
1269
          }
1270
          else {
1271
            b.bind( func.apply( newFileEditor ) );
1272
          }
1273
        }
1274
    );
1275
1276
    return b;
1277
  }
1278
1279
  //---- Convenience accessors ----------------------------------------------
1280
1281
  private Preferences getPreferences() {
1282
    return sOptions.getState();
1283
  }
1284
1285
  private int getCurrentParagraphIndex() {
1286
    return getActiveEditorPane().getCurrentParagraphIndex();
1287
  }
1288
1289
  private float getFloat( final String key, final float defaultValue ) {
1290
    return getPreferences().getFloat( key, defaultValue );
1291
  }
1292
1293
  public Window getWindow() {
1294
    return getScene().getWindow();
1295
  }
1296
1297
  private MarkdownEditorPane getActiveEditorPane() {
1298
    return getActiveFileEditorTab().getEditorPane();
1299
  }
1300
1301
  private FileEditorTab getActiveFileEditorTab() {
1302
    return getFileEditorPane().getActiveFileEditor();
1303
  }
1304
1305
  //---- Member accessors ---------------------------------------------------
1306
1307
  protected Scene getScene() {
1308
    return mScene;
1309
  }
1310
1311
  private SpellChecker getSpellChecker() {
1312
    return mSpellChecker;
1313
  }
1314
1315
  private Map<FileEditorTab, Processor<String>> getProcessors() {
1316
    return mProcessors;
1317
  }
1318
1319
  private FileEditorTabPane getFileEditorPane() {
1320
    return mFileEditorPane;
1321
  }
1322
1323
  private HTMLPreviewPane getPreviewPane() {
1324
    return mPreviewPane;
1325
  }
1326
1327
  private void setDefinitionSource(
1328
      final DefinitionSource definitionSource ) {
1329
    assert definitionSource != null;
1330
    mDefinitionSource = definitionSource;
1331
  }
1332
1333
  private DefinitionSource getDefinitionSource() {
1334
    return mDefinitionSource;
1335
  }
1336
1337
  private DefinitionPane getDefinitionPane() {
1338
    return mDefinitionPane;
1339
  }
1340
1341
  private Text getLineNumberText() {
1342
    return mLineNumberText;
1343
  }
1344
1345
  private StatusBar getStatusBar() {
1346
    return mStatusBar;
1347
  }
1348
1349
  private TextField getFindTextField() {
1350
    return mFindTextField;
1351
  }
1352
1353
  private DefinitionNameInjector getDefinitionNameInjector() {
1354
    return mDefinitionNameInjector;
1355
  }
1356
1357
  /**
1358
   * Returns the variable map of interpolated definitions.
1359
   *
1360
   * @return A map to help dereference variables.
1361
   */
1362
  private Map<String, String> getResolvedMap() {
1363
    return mResolvedMap;
1364
  }
1365
1366
  //---- Persistence accessors ----------------------------------------------
1367
1368
  private UserPreferences getUserPreferences() {
1369
    return UserPreferences.getInstance();
1370
  }
1371
1372
  private Path getDefinitionPath() {
1373
    return getUserPreferences().getDefinitionPath();
1374
  }
1375
1376
  //---- Spelling -----------------------------------------------------------
1377
1378
  /**
1379
   * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}.
1380
   * This is called to spell check the document, rather than a single paragraph.
1381
   *
1382
   * @param text The full document text.
1383
   */
1384
  private void spellcheck(
1385
      final StyleClassedTextArea editor, final String text ) {
1386
    spellcheck( editor, text, -1 );
1387
  }
1388
1389
  /**
1390
   * Spellchecks a subset of the entire document.
1391
   *
1392
   * @param text   Look up words for this text in the lexicon.
1393
   * @param paraId Set to -1 to apply resulting style spans to the entire
1394
   *               text.
1395
   */
1396
  private void spellcheck(
1397
      final StyleClassedTextArea editor, final String text, final int paraId ) {
1398
    final var builder = new StyleSpansBuilder<Collection<String>>();
1399
    final var runningIndex = new AtomicInteger( 0 );
1400
    final var checker = getSpellChecker();
1401
1402
    // The text nodes must be relayed through a contextual "visitor" that
1403
    // can return text in chunks with correlative offsets into the string.
1404
    // This allows Markdown, R Markdown, XML, and R XML documents to return
1405
    // sets of words to check.
1406
1407
    final var node = mParser.parse( text );
1408
    final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
1409
      // Treat hyphenated compound words as individual words.
1410
      final var check = visited.replace( '-', ' ' );
1411
1412
      checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
1413
        prevIndex += bIndex;
1414
        currIndex += bIndex;
1415
1416
        // Clear styling between lexiconically absent words.
1417
        builder.add( emptyList(), prevIndex - runningIndex.get() );
1418
        builder.add( singleton( "spelling" ), currIndex - prevIndex );
1419
        runningIndex.set( currIndex );
1420
      } );
1421
    } );
1422
1423
    visitor.visit( node );
1424
1425
    // If the running index was set, at least one word triggered the listener.
1426
    if( runningIndex.get() > 0 ) {
1427
      // Clear styling after the last lexiconically absent word.
1428
      builder.add( emptyList(), text.length() - runningIndex.get() );
1429
1430
      final var spans = builder.create();
1431
1432
      if( paraId >= 0 ) {
1433
        editor.setStyleSpans( paraId, 0, spans );
1434
      }
1435
      else {
1436
        editor.setStyleSpans( 0, spans );
1437
      }
1438
    }
1439
  }
1440
1441
  @SuppressWarnings("SameParameterValue")
1442
  private Collection<String> readLexicon( final String filename )
1443
      throws Exception {
1444
    final var path = "/" + LEXICONS_DIRECTORY + "/" + filename;
1445
1446
    try( final var resource = getClass().getResourceAsStream( path ) ) {
1447
      if( resource == null ) {
1448
        throw new MissingFileException( path );
1449
      }
1450
1451
      try( final var isr = new InputStreamReader( resource, UTF_8 );
1452
           final var reader = new BufferedReader( isr ) ) {
1453
        return reader.lines().collect( Collectors.toList() );
1454
      }
1455
    }
1456
  }
1457
1458
  // TODO: #59 -- Replace using Markdown processor instantiated for Markdown
1459
  //  files.
1460
  private final Parser mParser = Parser.builder().build();
1461
1462
  // TODO: #59 -- Replace with generic interface; provide Markdown/XML
1463
  //  implementations.
1464
  private static final class TextVisitor {
1465
    private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
1466
        com.vladsch.flexmark.ast.Text.class, this::visit )
1467
    );
1468
1469
    private final SpellCheckListener mConsumer;
1470
1471
    public TextVisitor( final SpellCheckListener consumer ) {
1472
      mConsumer = consumer;
1473
    }
1474
1475
    private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
1476
      if( node instanceof com.vladsch.flexmark.ast.Text ) {
1477
        mConsumer.accept( node.getChars().toString(),
1478
                          node.getStartOffset(),
1479
                          node.getEndOffset() );
1480
      }
1481
1482
      mVisitor.visitChildren( node );
1483
    }
1484
  }
1485
}
11486
A src/main/java/com/keenwrite/Messages.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  * Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  * Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite;
28
29
import java.text.MessageFormat;
30
import java.util.ResourceBundle;
31
import java.util.Stack;
32
33
import static com.keenwrite.Constants.APP_BUNDLE_NAME;
34
import static java.util.ResourceBundle.getBundle;
35
36
/**
37
 * Recursively resolves message properties. Property values can refer to other
38
 * properties using a <code>${var}</code> syntax.
39
 */
40
public class Messages {
41
42
  private static final ResourceBundle RESOURCE_BUNDLE =
43
      getBundle( APP_BUNDLE_NAME );
44
45
  private Messages() {
46
  }
47
48
  /**
49
   * Return the value of a resource bundle value after having resolved any
50
   * references to other bundle variables.
51
   *
52
   * @param props The bundle containing resolvable properties.
53
   * @param s     The value for a key to resolve.
54
   * @return The value of the key with all references recursively dereferenced.
55
   */
56
  @SuppressWarnings("SameParameterValue")
57
  private static String resolve( final ResourceBundle props, final String s ) {
58
    final int len = s.length();
59
    final Stack<StringBuilder> stack = new Stack<>();
60
61
    StringBuilder sb = new StringBuilder( 256 );
62
    boolean open = false;
63
64
    for( int i = 0; i < len; i++ ) {
65
      final char c = s.charAt( i );
66
67
      switch( c ) {
68
        case '$': {
69
          if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
70
            stack.push( sb );
71
            sb = new StringBuilder( 256 );
72
            i++;
73
            open = true;
74
          }
75
76
          break;
77
        }
78
79
        case '}': {
80
          if( open ) {
81
            open = false;
82
            final String name = sb.toString();
83
84
            sb = stack.pop();
85
            sb.append( props.getString( name ) );
86
            break;
87
          }
88
        }
89
90
        default: {
91
          sb.append( c );
92
          break;
93
        }
94
      }
95
    }
96
97
    if( open ) {
98
      throw new IllegalArgumentException( "missing '}'" );
99
    }
100
101
    return sb.toString();
102
  }
103
104
  /**
105
   * Returns the value for a key from the message bundle.
106
   *
107
   * @param key Retrieve the value for this key.
108
   * @return The value for the key.
109
   */
110
  public static String get( final String key ) {
111
    try {
112
      return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
113
    } catch( final Exception ex ) {
114
      return key;
115
    }
116
  }
117
118
  public static String getLiteral( final String key ) {
119
    return RESOURCE_BUNDLE.getString( key );
120
  }
121
122
  public static String get( final String key, final boolean interpolate ) {
123
    return interpolate ? get( key ) : getLiteral( key );
124
  }
125
126
  /**
127
   * Returns the value for a key from the message bundle with the arguments
128
   * replacing <code>{#}</code> place holders.
129
   *
130
   * @param key  Retrieve the value for this key.
131
   * @param args The values to substitute for place holders.
132
   * @return The value for the key.
133
   */
134
  public static String get( final String key, final Object... args ) {
135
    return MessageFormat.format( get( key ), args );
136
  }
137
}
1138
A src/main/java/com/keenwrite/ScrollEventHandler.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import javafx.beans.property.BooleanProperty;
31
import javafx.beans.property.SimpleBooleanProperty;
32
import javafx.event.Event;
33
import javafx.event.EventHandler;
34
import javafx.scene.Node;
35
import javafx.scene.control.ScrollBar;
36
import javafx.scene.control.skin.ScrollBarSkin;
37
import javafx.scene.input.MouseEvent;
38
import javafx.scene.input.ScrollEvent;
39
import javafx.scene.layout.StackPane;
40
import org.fxmisc.flowless.VirtualizedScrollPane;
41
import org.fxmisc.richtext.StyleClassedTextArea;
42
43
import javax.swing.*;
44
45
import static javafx.geometry.Orientation.VERTICAL;
46
47
/**
48
 * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to
49
 * an instance of {@link JScrollBar}.
50
 * <p>
51
 * Called to synchronize the scrolling areas for either scrolling with the
52
 * mouse or scrolling using the scrollbar's thumb. Both are required to avoid
53
 * scrolling on the estimatedScrollYProperty that occurs when text events
54
 * fire. Scrolling performed for text events are handled separately to ensure
55
 * the preview panel scrolls to the same position in the Markdown editor,
56
 * taking into account things like images, tables, and other potentially
57
 * long vertical presentation items.
58
 * </p>
59
 */
60
public final class ScrollEventHandler implements EventHandler<Event> {
61
62
  private final class MouseHandler implements EventHandler<MouseEvent> {
63
    private final EventHandler<? super MouseEvent> mOldHandler;
64
65
    /**
66
     * Constructs a new handler for mouse scrolling events.
67
     *
68
     * @param oldHandler Receives the event after scrolling takes place.
69
     */
70
    private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) {
71
      mOldHandler = oldHandler;
72
    }
73
74
    @Override
75
    public void handle( final MouseEvent event ) {
76
      ScrollEventHandler.this.handle( event );
77
      mOldHandler.handle( event );
78
    }
79
  }
80
81
  private final class ScrollHandler implements EventHandler<ScrollEvent> {
82
    @Override
83
    public void handle( final ScrollEvent event ) {
84
      ScrollEventHandler.this.handle( event );
85
    }
86
  }
87
88
  private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane;
89
  private final JScrollBar mPreviewScrollBar;
90
  private final BooleanProperty mEnabled = new SimpleBooleanProperty();
91
92
  /**
93
   * @param editorScrollPane Scroll event source (human movement).
94
   * @param previewScrollBar Scroll event destination (corresponding movement).
95
   */
96
  public ScrollEventHandler(
97
      final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane,
98
      final JScrollBar previewScrollBar ) {
99
    mEditorScrollPane = editorScrollPane;
100
    mPreviewScrollBar = previewScrollBar;
101
102
    mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
103
104
    final var thumb = getVerticalScrollBarThumb( mEditorScrollPane );
105
    thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) );
106
  }
107
108
  /**
109
   * Gets a property intended to be bound to selected property of the tab being
110
   * scrolled. This is required because there's only one preview pane but
111
   * multiple editor panes. Each editor pane maintains its own scroll position.
112
   *
113
   * @return A {@link BooleanProperty} representing whether the scroll
114
   * events for this tab are to be executed.
115
   */
116
  public BooleanProperty enabledProperty() {
117
    return mEnabled;
118
  }
119
120
  /**
121
   * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm
122
   * is based on Karl Tauber's ratio calculation.
123
   *
124
   * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent}
125
   */
126
  @Override
127
  public void handle( final Event event ) {
128
    if( isEnabled() ) {
129
      final var eScrollPane = getEditorScrollPane();
130
      final int eScrollY =
131
          eScrollPane.estimatedScrollYProperty().getValue().intValue();
132
      final int eHeight = (int)
133
          (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
134
              - eScrollPane.getHeight());
135
      final double eRatio = eHeight > 0
136
          ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
137
138
      final var pScrollBar = getPreviewScrollBar();
139
      final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
140
      final var pScrollY = (int) (pHeight * eRatio);
141
142
      pScrollBar.setValue( pScrollY );
143
      pScrollBar.getParent().repaint();
144
    }
145
  }
146
147
  private StackPane getVerticalScrollBarThumb(
148
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
149
    final ScrollBar scrollBar = getVerticalScrollBar( pane );
150
    final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get());
151
152
    for( final Node node : skin.getChildren() ) {
153
      // Brittle, but what can you do?
154
      if( node.getStyleClass().contains( "thumb" ) ) {
155
        return (StackPane) node;
156
      }
157
    }
158
159
    throw new IllegalArgumentException( "No scroll bar skin found." );
160
  }
161
162
  private ScrollBar getVerticalScrollBar(
163
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
164
165
    for( final Node node : pane.getChildrenUnmodifiable() ) {
166
      if( node instanceof ScrollBar ) {
167
        final ScrollBar scrollBar = (ScrollBar) node;
168
169
        if( scrollBar.getOrientation() == VERTICAL ) {
170
          return scrollBar;
171
        }
172
      }
173
    }
174
175
    throw new IllegalArgumentException( "No vertical scroll pane found." );
176
  }
177
178
  private boolean isEnabled() {
179
    // TODO: As a minor optimization, when this is set to false, it could remove
180
    // the MouseHandler and ScrollHandler so that events only dispatch to one
181
    // object (instead of one per editor tab).
182
    return mEnabled.get();
183
  }
184
185
  private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() {
186
    return mEditorScrollPane;
187
  }
188
189
  private JScrollBar getPreviewScrollBar() {
190
    return mPreviewScrollBar;
191
  }
192
}
1193
A src/main/java/com/keenwrite/Services.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
import java.util.ServiceLoader;
33
34
/**
35
 * Responsible for loading services. The services are treated as singleton
36
 * instances.
37
 */
38
public class Services {
39
40
  @SuppressWarnings("rawtypes")
41
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
42
43
  /**
44
   * Loads a service based on its interface definition. This will return an
45
   * existing instance if the class has already been instantiated.
46
   *
47
   * @param <T> The service to load.
48
   * @param api The interface definition for the service.
49
   * @return A class that implements the interface.
50
   */
51
  @SuppressWarnings("unchecked")
52
  public static <T> T load( final Class<T> api ) {
53
    final T o = (T) get( api );
54
55
    return o == null ? newInstance( api ) : o;
56
  }
57
58
  private static <T> T newInstance( final Class<T> api ) {
59
    final ServiceLoader<T> services = ServiceLoader.load( api );
60
61
    for( final T service : services ) {
62
      if( service != null ) {
63
        // Re-use the same instance the next time the class is loaded.
64
        put( api, service );
65
        return service;
66
      }
67
    }
68
69
    throw new RuntimeException( "No implementation for: " + api );
70
  }
71
72
  @SuppressWarnings("rawtypes")
73
  private static void put( final Class key, Object value ) {
74
    SINGLETONS.put( key, value );
75
  }
76
77
  @SuppressWarnings("rawtypes")
78
  private static Object get( final Class api ) {
79
    return SINGLETONS.get( api );
80
  }
81
}
182
A src/main/java/com/keenwrite/StatusBarNotifier.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite;
29
30
import com.keenwrite.service.events.Notifier;
31
import org.controlsfx.control.StatusBar;
32
33
import static com.keenwrite.Constants.STATUS_BAR_OK;
34
import static com.keenwrite.Messages.get;
35
import static javafx.application.Platform.runLater;
36
37
/**
38
 * Responsible for passing notifications about exceptions (or other error
39
 * messages) through the application. Once the Event Bus is implemented, this
40
 * class can go away.
41
 */
42
public class StatusBarNotifier {
43
  private static final String OK = get( STATUS_BAR_OK, "OK" );
44
45
  private static final Notifier sNotifier = Services.load( Notifier.class );
46
  private static StatusBar sStatusBar;
47
48
  public static void setStatusBar( final StatusBar statusBar ) {
49
    sStatusBar = statusBar;
50
  }
51
52
  /**
53
   * Resets the status bar to a default message.
54
   */
55
  public static void clearAlert() {
56
    // Don't burden the repaint thread if there's no status bar change.
57
    if( !OK.equals( sStatusBar.getText() ) ) {
58
      update( OK );
59
    }
60
  }
61
62
  /**
63
   * Updates the status bar with a custom message.
64
   *
65
   * @param key The resource bundle key associated with a message (typically
66
   *            to inform the user about an error).
67
   */
68
  public static void alert( final String key ) {
69
    update( get( key ) );
70
  }
71
72
  /**
73
   * Updates the status bar with a custom message.
74
   *
75
   * @param key  The property key having a value to populate with arguments.
76
   * @param args The placeholder values to substitute into the key's value.
77
   */
78
  public static void alert( final String key, final Object... args ) {
79
    update( get( key, args ) );
80
  }
81
82
  /**
83
   * Called when an exception occurs that warrants the user's attention.
84
   *
85
   * @param t The exception with a message that the user should know about.
86
   */
87
  public static void alert( final Throwable t ) {
88
    update( t.getMessage() );
89
  }
90
91
  /**
92
   * Updates the status bar to show the first line of the given message.
93
   *
94
   * @param message The message to show in the status bar.
95
   */
96
  private static void update( final String message ) {
97
    runLater(
98
        () -> {
99
          final var s = message == null ? "" : message;
100
          final var i = s.indexOf( '\n' );
101
          sStatusBar.setText( s.substring( 0, i > 0 ? i : s.length() ) );
102
        }
103
    );
104
  }
105
106
  /**
107
   * Returns the global {@link Notifier} instance that can be used for opening
108
   * pop-up alert messages.
109
   *
110
   * @return The pop-up {@link Notifier} dispatcher.
111
   */
112
  public static Notifier getNotifier() {
113
    return sNotifier;
114
  }
115
}
1116
A src/main/java/com/keenwrite/adapters/DocumentAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.adapters;
29
30
import org.xhtmlrenderer.event.DocumentListener;
31
32
import static com.keenwrite.StatusBarNotifier.alert;
33
34
/**
35
 * Allows subclasses to implement specific events.
36
 */
37
public class DocumentAdapter implements DocumentListener {
38
  @Override
39
  public void documentStarted() {
40
  }
41
42
  @Override
43
  public void documentLoaded() {
44
  }
45
46
  @Override
47
  public void onLayoutException( final Throwable t ) {
48
    alert( t );
49
  }
50
51
  @Override
52
  public void onRenderException( final Throwable t ) {
53
    alert( t );
54
  }
55
}
156
A src/main/java/com/keenwrite/adapters/ReplacedElementAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.adapters;
29
30
import org.w3c.dom.Element;
31
import org.xhtmlrenderer.extend.ReplacedElementFactory;
32
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
33
34
public abstract class ReplacedElementAdapter implements ReplacedElementFactory {
35
  @Override
36
  public void reset() {
37
  }
38
39
  @Override
40
  public void remove( final Element e ) {
41
  }
42
43
  @Override
44
  public void setFormSubmissionListener(
45
      final FormSubmissionListener listener ) {
46
  }
47
}
148
A src/main/java/com/keenwrite/controls/BrowseFileButton.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.keenwrite.controls;
29
30
import com.keenwrite.Messages;
31
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
32
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
33
import javafx.beans.property.ObjectProperty;
34
import javafx.beans.property.SimpleObjectProperty;
35
import javafx.event.ActionEvent;
36
import javafx.scene.control.Button;
37
import javafx.scene.control.Tooltip;
38
import javafx.scene.input.KeyCode;
39
import javafx.scene.input.KeyEvent;
40
import javafx.stage.FileChooser;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
43
import java.io.File;
44
import java.nio.file.Path;
45
import java.util.ArrayList;
46
import java.util.List;
47
48
/**
49
 * Button that opens a file chooser to select a local file for a URL.
50
 */
51
public class BrowseFileButton extends Button {
52
  private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
53
54
  public BrowseFileButton() {
55
    setGraphic(
56
        FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT )
57
    );
58
    setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
59
    setOnAction( this::browse );
60
61
    disableProperty().bind( basePath.isNull() );
62
63
    // workaround for a JavaFX bug:
64
    //   avoid closing the dialog that contains this control when the user
65
    //   closes the FileChooser or DirectoryChooser using the ESC key
66
    addEventHandler( KeyEvent.KEY_RELEASED, e -> {
67
      if( e.getCode() == KeyCode.ESCAPE ) {
68
        e.consume();
69
      }
70
    } );
71
  }
72
73
  public void addExtensionFilter( ExtensionFilter extensionFilter ) {
74
    extensionFilters.add( extensionFilter );
75
  }
76
77
  // 'basePath' property
78
  private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
79
80
  public Path getBasePath() {
81
    return basePath.get();
82
  }
83
84
  public void setBasePath( Path basePath ) {
85
    this.basePath.set( basePath );
86
  }
87
88
  // 'url' property
89
  private final ObjectProperty<String> url = new SimpleObjectProperty<>();
90
91
  public ObjectProperty<String> urlProperty() {
92
    return url;
93
  }
94
95
  protected void browse( ActionEvent e ) {
96
    FileChooser fileChooser = new FileChooser();
97
    fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
98
    fileChooser.getExtensionFilters().addAll( extensionFilters );
99
    fileChooser.getExtensionFilters()
100
               .add( new ExtensionFilter( Messages.get(
101
                   "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
102
    fileChooser.setInitialDirectory( getInitialDirectory() );
103
    File result = fileChooser.showOpenDialog( getScene().getWindow() );
104
    if( result != null ) {
105
      updateUrl( result );
106
    }
107
  }
108
109
  protected File getInitialDirectory() {
110
    //TODO build initial directory based on current value of 'url' property
111
    return getBasePath().toFile();
112
  }
113
114
  protected void updateUrl( File file ) {
115
    String newUrl;
116
    try {
117
      newUrl = getBasePath().relativize( file.toPath() ).toString();
118
    } catch( IllegalArgumentException ex ) {
119
      newUrl = file.toString();
120
    }
121
    url.set( newUrl.replace( '\\', '/' ) );
122
  }
123
}
1124
A src/main/java/com/keenwrite/controls/EscapeTextField.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.keenwrite.controls;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.TextField;
33
import javafx.util.StringConverter;
34
35
/**
36
 * Responsible for escaping/unescaping characters for markdown.
37
 */
38
public class EscapeTextField extends TextField {
39
40
  public EscapeTextField() {
41
    escapedText.bindBidirectional(
42
        textProperty(),
43
        new StringConverter<>() {
44
          @Override
45
          public String toString( String object ) {
46
            return escape( object );
47
          }
48
49
          @Override
50
          public String fromString( String string ) {
51
            return unescape( string );
52
          }
53
        }
54
    );
55
    escapeCharacters.addListener(
56
        e -> escapedText.set( escape( textProperty().get() ) )
57
    );
58
  }
59
60
  // 'escapedText' property
61
  private final StringProperty escapedText = new SimpleStringProperty();
62
63
  public StringProperty escapedTextProperty() {
64
    return escapedText;
65
  }
66
67
  // 'escapeCharacters' property
68
  private final StringProperty escapeCharacters = new SimpleStringProperty();
69
70
  public String getEscapeCharacters() {
71
    return escapeCharacters.get();
72
  }
73
74
  public void setEscapeCharacters( String escapeCharacters ) {
75
    this.escapeCharacters.set( escapeCharacters );
76
  }
77
78
  private String escape( final String s ) {
79
    final String escapeChars = getEscapeCharacters();
80
81
    return isEmpty( escapeChars ) ? s :
82
        s.replaceAll( "([" + escapeChars.replaceAll(
83
            "(.)",
84
            "\\\\$1" ) + "])", "\\\\$1" );
85
  }
86
87
  private String unescape( final String s ) {
88
    final String escapeChars = getEscapeCharacters();
89
90
    return isEmpty( escapeChars ) ? s :
91
        s.replaceAll( "\\\\([" + escapeChars
92
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
93
  }
94
95
  private static boolean isEmpty( final String s ) {
96
    return s == null || s.isEmpty();
97
  }
98
}
199
A src/main/java/com/keenwrite/definition/DefinitionFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import com.keenwrite.AbstractFileFactory;
31
import com.keenwrite.FileType;
32
import com.keenwrite.definition.yaml.YamlDefinitionSource;
33
34
import java.nio.file.Path;
35
36
import static com.keenwrite.Constants.GLOB_PREFIX_DEFINITION;
37
import static com.keenwrite.FileType.YAML;
38
import static com.keenwrite.util.ProtocolResolver.getProtocol;
39
40
/**
41
 * Responsible for creating objects that can read and write definition data
42
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
43
 * database.
44
 */
45
public class DefinitionFactory extends AbstractFileFactory {
46
47
  /**
48
   * Default (empty) constructor.
49
   */
50
  public DefinitionFactory() {
51
  }
52
53
  /**
54
   * Creates a definition source capable of reading definitions from the given
55
   * path.
56
   *
57
   * @param path Path to a resource containing definitions.
58
   * @return The definition source appropriate for the given path.
59
   */
60
  public DefinitionSource createDefinitionSource( final Path path ) {
61
    assert path != null;
62
63
    final var protocol = getProtocol( path.toString() );
64
    DefinitionSource result = null;
65
66
    if( protocol.isFile() ) {
67
      final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION );
68
      result = createFileDefinitionSource( filetype, path );
69
    }
70
    else {
71
      unknownFileType( protocol, path.toString() );
72
    }
73
74
    return result;
75
  }
76
77
  /**
78
   * Creates a definition source based on the file type.
79
   *
80
   * @param filetype Property key name suffix from settings.properties file.
81
   * @param path     Path to the file that corresponds to the extension.
82
   * @return A DefinitionSource capable of parsing the data stored at the path.
83
   */
84
  private DefinitionSource createFileDefinitionSource(
85
      final FileType filetype, final Path path ) {
86
    assert filetype != null;
87
    assert path != null;
88
89
    if( filetype == YAML ) {
90
      return new YamlDefinitionSource( path );
91
    }
92
93
    throw new IllegalArgumentException( filetype.toString() );
94
  }
95
}
196
A src/main/java/com/keenwrite/definition/DefinitionPane.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
31
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
32
import javafx.beans.property.SimpleStringProperty;
33
import javafx.beans.property.StringProperty;
34
import javafx.collections.ObservableList;
35
import javafx.event.ActionEvent;
36
import javafx.event.Event;
37
import javafx.event.EventHandler;
38
import javafx.geometry.Insets;
39
import javafx.geometry.Pos;
40
import javafx.scene.Node;
41
import javafx.scene.control.*;
42
import javafx.scene.input.KeyEvent;
43
import javafx.scene.layout.BorderPane;
44
import javafx.scene.layout.HBox;
45
import javafx.util.StringConverter;
46
47
import java.util.*;
48
49
import static com.keenwrite.Messages.get;
50
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
51
import static javafx.geometry.Pos.CENTER;
52
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
53
54
/**
55
 * Provides the user interface that holds a {@link TreeView}, which
56
 * allows users to interact with key/value pairs loaded from the
57
 * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
58
 */
59
public final class DefinitionPane extends BorderPane {
60
61
  /**
62
   * Contains a view of the definitions.
63
   */
64
  private final TreeView<String> mTreeView = new TreeView<>();
65
66
  /**
67
   * Handlers for key press events.
68
   */
69
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
70
      = new HashSet<>();
71
72
  /**
73
   * Definition file name shown in the title of the pane.
74
   */
75
  private final StringProperty mFilename = new SimpleStringProperty();
76
77
  private final TitledPane mTitledPane = new TitledPane();
78
79
  /**
80
   * Constructs a definition pane with a given tree view root.
81
   */
82
  public DefinitionPane() {
83
    final var treeView = getTreeView();
84
    treeView.setEditable( true );
85
    treeView.setCellFactory( cell -> createTreeCell() );
86
    treeView.setContextMenu( createContextMenu() );
87
    treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
88
    treeView.setShowRoot( false );
89
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
90
91
    final var bCreate = createButton(
92
        "create", TREE, e -> addItem() );
93
    final var bRename = createButton(
94
        "rename", EDIT, e -> editSelectedItem() );
95
    final var bDelete = createButton(
96
        "delete", TRASH, e -> deleteSelectedItems() );
97
98
    final var buttonBar = new HBox();
99
    buttonBar.getChildren().addAll( bCreate, bRename, bDelete );
100
    buttonBar.setAlignment( CENTER );
101
    buttonBar.setSpacing( 10 );
102
103
    final var titledPane = getTitledPane();
104
    titledPane.textProperty().bind( mFilename );
105
    titledPane.setContent( treeView );
106
    titledPane.setCollapsible( false );
107
    titledPane.setPadding( new Insets( 0, 0, 0, 0 ) );
108
109
    setTop( buttonBar );
110
    setCenter( titledPane );
111
    setAlignment( buttonBar, Pos.TOP_CENTER );
112
    setAlignment( titledPane, Pos.TOP_CENTER );
113
114
    titledPane.prefHeightProperty().bind( this.heightProperty() );
115
  }
116
117
  public void setTooltip( final Tooltip tooltip ) {
118
    getTitledPane().setTooltip( tooltip );
119
  }
120
121
  private TitledPane getTitledPane() {
122
    return mTitledPane;
123
  }
124
125
  private Button createButton(
126
      final String msgKey,
127
      final FontAwesomeIcon icon,
128
      final EventHandler<ActionEvent> eventHandler ) {
129
    final var keyPrefix = "Pane.definition.button." + msgKey;
130
    final var button = new Button( get( keyPrefix + ".label" ) );
131
    button.setOnAction( eventHandler );
132
133
    button.setGraphic(
134
        FontAwesomeIconFactory.get().createIcon( icon )
135
    );
136
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
137
138
    return button;
139
  }
140
141
  /**
142
   * Changes the root of the {@link TreeView} to the root of the
143
   * {@link TreeView} from the {@link DefinitionSource}.
144
   *
145
   * @param definitionSource Container for the hierarchy of key/value pairs
146
   *                         to replace the existing hierarchy.
147
   */
148
  public void update( final DefinitionSource definitionSource ) {
149
    assert definitionSource != null;
150
151
    final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
152
    final TreeItem<String> root = treeAdapter.adapt(
153
        get( "Pane.definition.node.root.title" )
154
    );
155
156
    getTreeView().setRoot( root );
157
  }
158
159
  public Map<String, String> toMap() {
160
    return TreeItemAdapter.toMap( getTreeView().getRoot() );
161
  }
162
163
  /**
164
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
165
   * is modified. The modifications include: item value changes, item additions,
166
   * and item removals.
167
   * <p>
168
   * Safe to call multiple times; if a handler is already registered, the
169
   * old handler is used.
170
   * </p>
171
   *
172
   * @param handler The handler to call whenever any {@link TreeItem} changes.
173
   */
174
  public void addTreeChangeHandler(
175
      final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
176
    final TreeItem<String> root = getTreeView().getRoot();
177
    root.addEventHandler( TreeItem.valueChangedEvent(), handler );
178
    root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
179
  }
180
181
  public void addKeyEventHandler(
182
      final EventHandler<? super KeyEvent> handler ) {
183
    getKeyEventHandlers().add( handler );
184
  }
185
186
  /**
187
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
188
   * well-formed for export. A tree is considered well-formed if the following
189
   * conditions are met:
190
   *
191
   * <ul>
192
   *   <li>The root node contains at least one child node having a leaf.</li>
193
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
194
   * </ul>
195
   *
196
   * @return {@code null} if the document is well-formed, otherwise the
197
   * problematic child {@link TreeItem}.
198
   */
199
  public TreeItem<String> isTreeWellFormed() {
200
    final var root = getTreeView().getRoot();
201
202
    for( final var child : root.getChildren() ) {
203
      final var problemChild = isWellFormed( child );
204
205
      if( child.isLeaf() || problemChild != null ) {
206
        return problemChild;
207
      }
208
    }
209
210
    return null;
211
  }
212
213
  /**
214
   * Determines whether the document is well-formed by ensuring that
215
   * child branches do not contain multiple leaves.
216
   *
217
   * @param item The sub-tree to check for well-formedness.
218
   * @return {@code null} when the tree is well-formed, otherwise the
219
   * problematic {@link TreeItem}.
220
   */
221
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
222
    int childLeafs = 0;
223
    int childBranches = 0;
224
225
    for( final TreeItem<String> child : item.getChildren() ) {
226
      if( child.isLeaf() ) {
227
        childLeafs++;
228
      }
229
      else {
230
        childBranches++;
231
      }
232
233
      final var problemChild = isWellFormed( child );
234
235
      if( problemChild != null ) {
236
        return problemChild;
237
      }
238
    }
239
240
    return ((childBranches > 0 && childLeafs == 0) ||
241
        (childBranches == 0 && childLeafs <= 1)) ? null : item;
242
  }
243
244
  /**
245
   * Delegates to {@link DefinitionTreeItem#findLeafExact(String)}.
246
   *
247
   * @param text The value to find, never {@code null}.
248
   * @return The leaf that contains the given value, or {@code null} if
249
   * not found.
250
   */
251
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
252
    return getTreeRoot().findLeafExact( text );
253
  }
254
255
  /**
256
   * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
257
   *
258
   * @param text The value to find, never {@code null}.
259
   * @return The leaf that contains the given value, or {@code null} if
260
   * not found.
261
   */
262
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
263
    return getTreeRoot().findLeafContains( text );
264
  }
265
266
  /**
267
   * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
268
   *
269
   * @param text The value to find, never {@code null}.
270
   * @return The leaf that contains the given value, or {@code null} if
271
   * not found.
272
   */
273
  public DefinitionTreeItem<String> findLeafContainsNoCase(
274
      final String text ) {
275
    return getTreeRoot().findLeafContainsNoCase( text );
276
  }
277
278
  /**
279
   * Delegates to {@link DefinitionTreeItem#findLeafStartsWith(String)}.
280
   *
281
   * @param text The value to find, never {@code null}.
282
   * @return The leaf that contains the given value, or {@code null} if
283
   * not found.
284
   */
285
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
286
    return getTreeRoot().findLeafStartsWith( text );
287
  }
288
289
  /**
290
   * Expands the node to the root, recursively.
291
   *
292
   * @param <T>  The type of tree item to expand (usually String).
293
   * @param node The node to expand.
294
   */
295
  public <T> void expand( final TreeItem<T> node ) {
296
    if( node != null ) {
297
      expand( node.getParent() );
298
299
      if( !node.isLeaf() ) {
300
        node.setExpanded( true );
301
      }
302
    }
303
  }
304
305
  public void select( final TreeItem<String> item ) {
306
    getSelectionModel().clearSelection();
307
    getSelectionModel().select( getTreeView().getRow( item ) );
308
  }
309
310
  /**
311
   * Collapses the tree, recursively.
312
   */
313
  public void collapse() {
314
    collapse( getTreeRoot().getChildren() );
315
  }
316
317
  /**
318
   * Collapses the tree, recursively.
319
   *
320
   * @param <T>   The type of tree item to expand (usually String).
321
   * @param nodes The nodes to collapse.
322
   */
323
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
324
    for( final var node : nodes ) {
325
      node.setExpanded( false );
326
      collapse( node.getChildren() );
327
    }
328
  }
329
330
  /**
331
   * @return {@code true} when the user is editing a {@link TreeItem}.
332
   */
333
  private boolean isEditingTreeItem() {
334
    return getTreeView().editingItemProperty().getValue() != null;
335
  }
336
337
  /**
338
   * Changes to edit mode for the selected item.
339
   */
340
  private void editSelectedItem() {
341
    getTreeView().edit( getSelectedItem() );
342
  }
343
344
  /**
345
   * Removes all selected items from the {@link TreeView}.
346
   */
347
  private void deleteSelectedItems() {
348
    for( final var item : getSelectedItems() ) {
349
      final var parent = item.getParent();
350
351
      if( parent != null ) {
352
        parent.getChildren().remove( item );
353
      }
354
    }
355
  }
356
357
  /**
358
   * Deletes the selected item.
359
   */
360
  private void deleteSelectedItem() {
361
    final var c = getSelectedItem();
362
    getSiblings( c ).remove( c );
363
  }
364
365
  /**
366
   * Adds a new item under the selected item (or root if nothing is selected).
367
   * There are a few conditions to consider: when adding to the root,
368
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
369
   * root must contain two items: a key and a value.
370
   */
371
  public void addItem() {
372
    final var value = createTreeItem();
373
    getSelectedItem().getChildren().add( value );
374
    expand( value );
375
    select( value );
376
  }
377
378
  private ContextMenu createContextMenu() {
379
    final ContextMenu menu = new ContextMenu();
380
    final ObservableList<MenuItem> items = menu.getItems();
381
382
    addMenuItem( items, "Definition.menu.create" )
383
        .setOnAction( e -> addItem() );
384
385
    addMenuItem( items, "Definition.menu.rename" )
386
        .setOnAction( e -> editSelectedItem() );
387
388
    addMenuItem( items, "Definition.menu.remove" )
389
        .setOnAction( e -> deleteSelectedItem() );
390
391
    return menu;
392
  }
393
394
  /**
395
   * Executes hot-keys for edits to the definition tree.
396
   *
397
   * @param event Contains the key code of the key that was pressed.
398
   */
399
  private void keyEventFilter( final KeyEvent event ) {
400
    if( !isEditingTreeItem() ) {
401
      switch( event.getCode() ) {
402
        case ENTER:
403
          expand( getSelectedItem() );
404
          event.consume();
405
          break;
406
407
        case DELETE:
408
          deleteSelectedItems();
409
          break;
410
411
        case INSERT:
412
          addItem();
413
          break;
414
415
        case R:
416
          if( event.isControlDown() ) {
417
            editSelectedItem();
418
          }
419
420
          break;
421
      }
422
423
      for( final var handler : getKeyEventHandlers() ) {
424
        handler.handle( event );
425
      }
426
    }
427
  }
428
429
  /**
430
   * Adds a menu item to a list of menu items.
431
   *
432
   * @param items    The list of menu items to append to.
433
   * @param labelKey The resource bundle key name for the menu item's label.
434
   * @return The menu item added to the list of menu items.
435
   */
436
  private MenuItem addMenuItem(
437
      final List<MenuItem> items, final String labelKey ) {
438
    final MenuItem menuItem = createMenuItem( labelKey );
439
    items.add( menuItem );
440
    return menuItem;
441
  }
442
443
  private MenuItem createMenuItem( final String labelKey ) {
444
    return new MenuItem( get( labelKey ) );
445
  }
446
447
  private DefinitionTreeItem<String> createTreeItem() {
448
    return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
449
  }
450
451
  private TreeCell<String> createTreeCell() {
452
    return new FocusAwareTextFieldTreeCell( createStringConverter() ) {
453
      @Override
454
      public void commitEdit( final String newValue ) {
455
        super.commitEdit( newValue );
456
        select( getTreeItem() );
457
        requestFocus();
458
      }
459
    };
460
  }
461
462
  @Override
463
  public void requestFocus() {
464
    super.requestFocus();
465
    getTreeView().requestFocus();
466
  }
467
468
  private StringConverter<String> createStringConverter() {
469
    return new StringConverter<>() {
470
      @Override
471
      public String toString( final String object ) {
472
        return object == null ? "" : object;
473
      }
474
475
      @Override
476
      public String fromString( final String string ) {
477
        return string == null ? "" : string;
478
      }
479
    };
480
  }
481
482
  /**
483
   * Returns the tree view that contains the definition hierarchy.
484
   *
485
   * @return A non-null instance.
486
   */
487
  public TreeView<String> getTreeView() {
488
    return mTreeView;
489
  }
490
491
  /**
492
   * Returns this pane.
493
   *
494
   * @return this
495
   */
496
  public Node getNode() {
497
    return this;
498
  }
499
500
  /**
501
   * Returns the property used to set the title of the pane: the file name.
502
   *
503
   * @return A non-null property used for showing the definition file name.
504
   */
505
  public StringProperty filenameProperty() {
506
    return mFilename;
507
  }
508
509
  /**
510
   * Returns the root of the tree.
511
   *
512
   * @return The first node added to the definition tree.
513
   */
514
  private DefinitionTreeItem<String> getTreeRoot() {
515
    final var root = getTreeView().getRoot();
516
517
    return root instanceof DefinitionTreeItem
518
        ? (DefinitionTreeItem<String>) root
519
        : new DefinitionTreeItem<>( "root" );
520
  }
521
522
  private ObservableList<TreeItem<String>> getSiblings(
523
      final TreeItem<String> item ) {
524
    final var root = getTreeView().getRoot();
525
    final var parent = (item == null || item == root) ? root : item.getParent();
526
527
    return parent.getChildren();
528
  }
529
530
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
531
    return getTreeView().getSelectionModel();
532
  }
533
534
  /**
535
   * Returns a copy of all the selected items.
536
   *
537
   * @return A list, possibly empty, containing all selected items in the
538
   * {@link TreeView}.
539
   */
540
  private List<TreeItem<String>> getSelectedItems() {
541
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
542
  }
543
544
  public TreeItem<String> getSelectedItem() {
545
    final var item = getSelectionModel().getSelectedItem();
546
    return item == null ? getTreeView().getRoot() : item;
547
  }
548
549
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
550
    return mKeyEventHandlers;
551
  }
552
553
  /**
554
   * Answers whether there are any definitions in the tree.
555
   *
556
   * @return {@code true} when there are no definitions; {@code false} when
557
   * there's at least one definition.
558
   */
559
  public boolean isEmpty() {
560
    return getTreeRoot().isEmpty();
561
  }
562
}
1563
A src/main/java/com/keenwrite/definition/DefinitionSource.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
/**
31
 * Represents behaviours for reading and writing string definitions. This
32
 * class cannot have any direct hooks into the user interface, as it defines
33
 * entry points into the definition data model loaded into an object
34
 * hierarchy. That hierarchy is converted to a UI model using an adapter
35
 * pattern.
36
 */
37
public interface DefinitionSource {
38
39
  /**
40
   * Creates an object capable of producing view-based objects from this
41
   * definition source.
42
   *
43
   * @return A hierarchical tree suitable for displaying in the definition pane.
44
   */
45
  TreeAdapter getTreeAdapter();
46
}
147
A src/main/java/com/keenwrite/definition/DefinitionTreeItem.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import javafx.scene.control.TreeItem;
31
32
import java.util.Stack;
33
import java.util.function.BiFunction;
34
35
import static java.text.Normalizer.Form.NFD;
36
import static java.text.Normalizer.normalize;
37
38
/**
39
 * Provides behaviour afforded to definition keys and corresponding value.
40
 *
41
 * @param <T> The type of {@link TreeItem} (usually string).
42
 */
43
public class DefinitionTreeItem<T> extends TreeItem<T> {
44
45
  /**
46
   * Constructs a new item with a default value.
47
   *
48
   * @param value Passed up to superclass.
49
   */
50
  public DefinitionTreeItem( final T value ) {
51
    super( value );
52
  }
53
54
  /**
55
   * Finds a leaf starting at the current node with text that matches the given
56
   * value. Search is performed case-sensitively.
57
   *
58
   * @param text The text to match against each leaf in the tree.
59
   * @return The leaf that has a value exactly matching the given text.
60
   */
61
  public DefinitionTreeItem<T> findLeafExact( final String text ) {
62
    return findLeaf( text, DefinitionTreeItem::valueEquals );
63
  }
64
65
  /**
66
   * Finds a leaf starting at the current node with text that matches the given
67
   * value. Search is performed case-sensitively.
68
   *
69
   * @param text The text to match against each leaf in the tree.
70
   * @return The leaf that has a value that contains the given text.
71
   */
72
  public DefinitionTreeItem<T> findLeafContains( final String text ) {
73
    return findLeaf( text, DefinitionTreeItem::valueContains );
74
  }
75
76
  /**
77
   * Finds a leaf starting at the current node with text that matches the given
78
   * value. Search is performed case-insensitively.
79
   *
80
   * @param text The text to match against each leaf in the tree.
81
   * @return The leaf that has a value that contains the given text.
82
   */
83
  public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
84
    return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
85
  }
86
87
  /**
88
   * Finds a leaf starting at the current node with text that matches the given
89
   * value. Search is performed case-sensitively.
90
   *
91
   * @param text The text to match against each leaf in the tree.
92
   * @return The leaf that has a value that starts with the given text.
93
   */
94
  public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
95
    return findLeaf( text, DefinitionTreeItem::valueStartsWith );
96
  }
97
98
  /**
99
   * Finds a leaf starting at the current node with text that matches the given
100
   * value.
101
   *
102
   * @param text     The text to match against each leaf in the tree.
103
   * @param findMode What algorithm is used to match the given text.
104
   * @return The leaf that has a value starting with the given text, or {@code
105
   * null} if there was no match found.
106
   */
107
  public DefinitionTreeItem<T> findLeaf(
108
      final String text,
109
      final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
110
    final var stack = new Stack<DefinitionTreeItem<T>>();
111
    stack.push( this );
112
113
    // Don't hunt for blank (empty) keys.
114
    boolean found = text.isBlank();
115
116
    while( !found && !stack.isEmpty() ) {
117
      final var node = stack.pop();
118
119
      for( final var child : node.getChildren() ) {
120
        final var result = (DefinitionTreeItem<T>) child;
121
122
        if( result.isLeaf() ) {
123
          if( found = findMode.apply( result, text ) ) {
124
            return result;
125
          }
126
        }
127
        else {
128
          stack.push( result );
129
        }
130
      }
131
    }
132
133
    return null;
134
  }
135
136
  /**
137
   * Returns the value of the string without diacritic marks.
138
   *
139
   * @return A non-null, possibly empty string.
140
   */
141
  private String getDiacriticlessValue() {
142
    return normalize( getValue().toString(), NFD )
143
        .replaceAll( "\\p{M}", "" );
144
  }
145
146
  /**
147
   * Returns true if this node is a leaf and its value equals the given text.
148
   *
149
   * @param s The text to compare against the node value.
150
   * @return true Node is a leaf and its value equals the given value.
151
   */
152
  private boolean valueEquals( final String s ) {
153
    return isLeaf() && getValue().equals( s );
154
  }
155
156
  /**
157
   * Returns true if this node is a leaf and its value contains the given text.
158
   *
159
   * @param s The text to compare against the node value.
160
   * @return true Node is a leaf and its value contains the given value.
161
   */
162
  private boolean valueContains( final String s ) {
163
    return isLeaf() && getDiacriticlessValue().contains( s );
164
  }
165
166
  /**
167
   * Returns true if this node is a leaf and its value contains the given text.
168
   *
169
   * @param s The text to compare against the node value.
170
   * @return true Node is a leaf and its value contains the given value.
171
   */
172
  private boolean valueContainsNoCase( final String s ) {
173
    return isLeaf() && getDiacriticlessValue()
174
        .toLowerCase().contains( s.toLowerCase() );
175
  }
176
177
  /**
178
   * Returns true if this node is a leaf and its value starts with the given
179
   * text.
180
   *
181
   * @param s The text to compare against the node value.
182
   * @return true Node is a leaf and its value starts with the given value.
183
   */
184
  private boolean valueStartsWith( final String s ) {
185
    return isLeaf() && getDiacriticlessValue().startsWith( s );
186
  }
187
188
  /**
189
   * Returns the path for this node, with nodes made distinct using the
190
   * separator character. This uses two loops: one for pushing nodes onto a
191
   * stack and one for popping them off to create the path in desired order.
192
   *
193
   * @return A non-null string, possibly empty.
194
   */
195
  public String toPath() {
196
    return TreeItemAdapter.toPath( getParent() );
197
  }
198
199
  /**
200
   * Answers whether there are any definitions in this tree.
201
   *
202
   * @return {@code true} when there are no definitions in the tree; {@code
203
   * false} when there is at least one definition present.
204
   */
205
  public boolean isEmpty() {
206
    return getChildren().isEmpty();
207
  }
208
}
1209
A src/main/java/com/keenwrite/definition/DocumentParser.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
/**
31
 * Responsible for parsing structured document formats.
32
 *
33
 * @param <T> The type of "node" for the document's object model.
34
 */
35
public interface DocumentParser<T> {
36
37
  /**
38
   * Parses a document into a nested object hierarchy. The object returned
39
   * from this call must be the root node in the document tree.
40
   *
41
   * @return The document's root node, which may be empty but never null.
42
   */
43
  T getDocumentRoot();
44
}
145
A src/main/java/com/keenwrite/definition/FocusAwareTextFieldTreeCell.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import javafx.scene.Node;
31
import javafx.scene.control.TextField;
32
import javafx.scene.control.cell.TextFieldTreeCell;
33
import javafx.util.StringConverter;
34
35
/**
36
 * Responsible for fixing a focus lost bug in the JavaFX implementation.
37
 * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details.
38
 * This implementation borrows from the official documentation on creating
39
 * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm
40
 */
41
public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> {
42
  private TextField mTextField;
43
44
  public FocusAwareTextFieldTreeCell(
45
      final StringConverter<String> converter ) {
46
    super( converter );
47
  }
48
49
  @Override
50
  public void startEdit() {
51
    super.startEdit();
52
    var textField = mTextField;
53
54
    if( textField == null ) {
55
      textField = createTextField();
56
    }
57
    else {
58
      textField.setText( getItem() );
59
    }
60
61
    setText( null );
62
    setGraphic( textField );
63
    textField.selectAll();
64
    textField.requestFocus();
65
66
    // When the focus is lost, commit the edit then close the input field.
67
    // This fixes the unexpected behaviour when user clicks away.
68
    textField.focusedProperty().addListener( ( l, o, n ) -> {
69
      if( !n ) {
70
        commitEdit( mTextField.getText() );
71
      }
72
    } );
73
74
    mTextField = textField;
75
  }
76
77
  @Override
78
  public void cancelEdit() {
79
    super.cancelEdit();
80
    setText( getItem() );
81
    setGraphic( getTreeItem().getGraphic() );
82
  }
83
84
  @Override
85
  public void updateItem( String item, boolean empty ) {
86
    super.updateItem( item, empty );
87
88
    String text = null;
89
    Node graphic = null;
90
91
    if( !empty ) {
92
      if( isEditing() ) {
93
        final var textField = mTextField;
94
95
        if( textField != null ) {
96
          textField.setText( getString() );
97
        }
98
99
        graphic = textField;
100
      }
101
      else {
102
        text = getString();
103
        graphic = getTreeItem().getGraphic();
104
      }
105
    }
106
107
    setText( text );
108
    setGraphic( graphic );
109
  }
110
111
  private TextField createTextField() {
112
    final var textField = new TextField( getString() );
113
114
    textField.setOnKeyReleased( t -> {
115
      switch( t.getCode() ) {
116
        case ENTER -> commitEdit( textField.getText() );
117
        case ESCAPE -> cancelEdit();
118
      }
119
    } );
120
121
    return textField;
122
  }
123
124
  private String getString() {
125
    return getConverter().toString( getItem() );
126
  }
127
}
1128
A src/main/java/com/keenwrite/definition/MapInterpolator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import com.keenwrite.sigils.YamlSigilOperator;
31
32
import java.util.Map;
33
import java.util.regex.Matcher;
34
35
import static com.keenwrite.sigils.YamlSigilOperator.REGEX_PATTERN;
36
37
/**
38
 * Responsible for performing string interpolation on key/value pairs stored
39
 * in a map. The values in the map can use a delimited syntax to refer to
40
 * keys in the map.
41
 */
42
public class MapInterpolator {
43
  private static final int GROUP_DELIMITED = 1;
44
45
  /**
46
   * Empty.
47
   */
48
  private MapInterpolator() {
49
  }
50
51
  /**
52
   * Performs string interpolation on the values in the given map. This will
53
   * change any value in the map that contains a variable that matches
54
   * {@link YamlSigilOperator#REGEX_PATTERN}.
55
   *
56
   * @param map Contains values that represent references to keys.
57
   */
58
  public static void interpolate( final Map<String, String> map ) {
59
    map.replaceAll( ( k, v ) -> resolve( map, v ) );
60
  }
61
62
  /**
63
   * Given a value with zero or more key references, this will resolve all
64
   * the values, recursively. If a key cannot be dereferenced, the value will
65
   * contain the key name.
66
   *
67
   * @param map   Map to search for keys when resolving key references.
68
   * @param value Value containing zero or more key references
69
   * @return The given value with all embedded key references interpolated.
70
   */
71
  private static String resolve(
72
      final Map<String, String> map, String value ) {
73
    final Matcher matcher = REGEX_PATTERN.matcher( value );
74
75
    while( matcher.find() ) {
76
      final String keyName = matcher.group( GROUP_DELIMITED );
77
      final String mapValue = map.get( keyName );
78
      final String keyValue = mapValue == null
79
          ? keyName
80
          : resolve( map, mapValue );
81
82
      value = value.replace( keyName, keyValue );
83
    }
84
85
    return value;
86
  }
87
}
188
A src/main/java/com/keenwrite/definition/RootTreeItem.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import javafx.scene.control.TreeItem;
31
import javafx.scene.control.TreeView;
32
33
/**
34
 * Indicates that this is the top-most {@link TreeItem}. This class allows
35
 * the {@link TreeItemAdapter} to ignore the topmost definition. Such
36
 * contortions are necessary because {@link TreeView} requires a root item
37
 * that isn't part of the user's definition file.
38
 * <p>
39
 * Another approach would be to associate object pairs per {@link TreeItem},
40
 * but that would be a waste of memory since the only "exception" case is
41
 * the root {@link TreeItem}.
42
 * </p>
43
 *
44
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
45
 */
46
public class RootTreeItem<T> extends DefinitionTreeItem<T> {
47
  /**
48
   * Default constructor, calls the superclass, no other behaviour.
49
   *
50
   * @param value The {@link TreeItem} node name to construct the superclass.
51
   * @see TreeItemAdapter#toMap(TreeItem) for details on how this
52
   * class is used.
53
   */
54
  public RootTreeItem( final T value ) {
55
    super( value );
56
  }
57
}
158
A src/main/java/com/keenwrite/definition/TreeAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import javafx.scene.control.TreeItem;
31
32
import java.io.IOException;
33
import java.nio.file.Path;
34
35
/**
36
 * Responsible for converting an object hierarchy into a {@link TreeItem}
37
 * hierarchy.
38
 */
39
public interface TreeAdapter {
40
  /**
41
   * Adapts the document produced by the given parser into a {@link TreeItem}
42
   * object that can be presented to the user within a GUI.
43
   *
44
   * @param root The default root node name.
45
   * @return The parsed document in a {@link TreeItem} that can be displayed
46
   * in a panel.
47
   */
48
  TreeItem<String> adapt( String root );
49
50
  /**
51
   * Exports the given root node to the given path.
52
   *
53
   * @param root The root node to export.
54
   * @param path Where to persist the data.
55
   * @throws IOException Could not write the data to the given path.
56
   */
57
  void export( TreeItem<String> root, Path path ) throws IOException;
58
}
159
A src/main/java/com/keenwrite/definition/TreeItemAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.keenwrite.sigils.YamlSigilOperator;
32
import com.keenwrite.preview.HTMLPreviewPane;
33
import javafx.scene.control.TreeItem;
34
import javafx.scene.control.TreeView;
35
36
import java.util.HashMap;
37
import java.util.Iterator;
38
import java.util.Map;
39
import java.util.Stack;
40
41
import static com.keenwrite.Constants.DEFAULT_MAP_SIZE;
42
43
/**
44
 * Given a {@link TreeItem}, this will generate a flat map with all the
45
 * values in the tree recursively interpolated. The application integrates
46
 * definition files as follows:
47
 * <ol>
48
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
49
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
50
 *   <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
51
 *   <li>Substitute flat map variables into document as required.</li>
52
 * </ol>
53
 *
54
 * <p>
55
 * This class is responsible for producing the interpolated flat map. This
56
 * allows dynamic edits of the {@link TreeView} to be displayed in the
57
 * {@link HTMLPreviewPane} without having to reload the definition file.
58
 * Reloading the definition file would work, but has a number of drawbacks.
59
 * </p>
60
 */
61
public class TreeItemAdapter {
62
  /**
63
   * Separates YAML definition keys (e.g., the dots in {@code $root.node.var$}).
64
   */
65
  public static final String SEPARATOR = ".";
66
67
  /**
68
   * Default buffer length for keys ({@link StringBuilder} has 16 character
69
   * buffer) that should be large enough for most keys to avoid reallocating
70
   * memory to increase the {@link StringBuilder}'s buffer.
71
   */
72
  public static final int DEFAULT_KEY_LENGTH = 64;
73
74
  /**
75
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
76
   * as a consecutive list.
77
   */
78
  private static final class TreeIterator
79
      implements Iterator<TreeItem<String>> {
80
    private final Stack<TreeItem<String>> mStack = new Stack<>();
81
82
    public TreeIterator( final TreeItem<String> root ) {
83
      if( root != null ) {
84
        mStack.push( root );
85
      }
86
    }
87
88
    @Override
89
    public boolean hasNext() {
90
      return !mStack.isEmpty();
91
    }
92
93
    @Override
94
    public TreeItem<String> next() {
95
      final TreeItem<String> next = mStack.pop();
96
      next.getChildren().forEach( mStack::push );
97
98
      return next;
99
    }
100
  }
101
102
  private TreeItemAdapter() {
103
  }
104
105
  /**
106
   * Iterate over a given root node (at any level of the tree) and process each
107
   * leaf node into a flat map. Values must be interpolated separately.
108
   */
109
  public static Map<String, String> toMap( final TreeItem<String> root ) {
110
    final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
111
    final TreeIterator iterator = new TreeIterator( root );
112
113
    iterator.forEachRemaining( item -> {
114
      if( item.isLeaf() ) {
115
        map.put( toPath( item.getParent() ), item.getValue() );
116
      }
117
    } );
118
119
    return map;
120
  }
121
122
123
  /**
124
   * For a given node, this will ascend the tree to generate a key name
125
   * that is associated with the leaf node's value.
126
   *
127
   * @param node Ascendants represent the key to this node's value.
128
   * @param <T>  Data type that the {@link TreeItem} contains.
129
   * @return The string representation of the node's unique key.
130
   */
131
  public static <T> String toPath( TreeItem<T> node ) {
132
    assert node != null;
133
134
    final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH );
135
    final Stack<TreeItem<T>> stack = new Stack<>();
136
137
    while( node != null && !(node instanceof RootTreeItem) ) {
138
      stack.push( node );
139
      node = node.getParent();
140
    }
141
142
    // Gets set at end of first iteration (to avoid an if condition).
143
    String separator = "";
144
145
    while( !stack.empty() ) {
146
      final T subkey = stack.pop().getValue();
147
      key.append( separator );
148
      key.append( subkey );
149
      separator = SEPARATOR;
150
    }
151
152
    return YamlSigilOperator.entoken( key.toString() );
153
  }
154
}
1155
A src/main/java/com/keenwrite/definition/yaml/YamlDefinitionSource.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition.yaml;
29
30
import com.keenwrite.definition.DefinitionSource;
31
import com.keenwrite.definition.TreeAdapter;
32
33
import java.nio.file.Path;
34
35
/**
36
 * Represents a definition data source for YAML files.
37
 */
38
public class YamlDefinitionSource implements DefinitionSource {
39
40
  private final YamlTreeAdapter mYamlTreeAdapter;
41
42
  /**
43
   * Constructs a new YAML definition source, populated from the given file.
44
   *
45
   * @param path Path to the YAML definition file.
46
   */
47
  public YamlDefinitionSource( final Path path ) {
48
    assert path != null;
49
50
    mYamlTreeAdapter = new YamlTreeAdapter( path );
51
  }
52
53
  @Override
54
  public TreeAdapter getTreeAdapter() {
55
    return mYamlTreeAdapter;
56
  }
57
}
158
A src/main/java/com/keenwrite/definition/yaml/YamlParser.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.ObjectMapper;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
33
import com.keenwrite.definition.DocumentParser;
34
35
import java.io.InputStream;
36
import java.nio.file.Files;
37
import java.nio.file.Path;
38
39
/**
40
 * Responsible for reading a YAML document into an object hierarchy.
41
 */
42
public class YamlParser implements DocumentParser<JsonNode> {
43
44
  /**
45
   * Start of the Universe (the YAML document node that contains all others).
46
   */
47
  private final JsonNode mDocumentRoot;
48
49
  /**
50
   * Creates a new YamlParser instance that attempts to parse the contents
51
   * of the YAML document given from a path. In the event that the file either
52
   * does not exist or is empty, a fake
53
   *
54
   * @param path Path to a file containing YAML data to parse.
55
   */
56
  public YamlParser( final Path path ) {
57
    assert path != null;
58
    mDocumentRoot = parse( path );
59
  }
60
61
  /**
62
   * Returns the parent node for the entire YAML document tree.
63
   *
64
   * @return The document root, never {@code null}.
65
   */
66
  @Override
67
  public JsonNode getDocumentRoot() {
68
    return mDocumentRoot;
69
  }
70
71
  /**
72
   * Parses the given path containing YAML data into an object hierarchy.
73
   *
74
   * @param path {@link Path} to the YAML resource to parse.
75
   * @return The parsed contents, or an empty object hierarchy.
76
   */
77
  private JsonNode parse( final Path path ) {
78
    try( final InputStream in = Files.newInputStream( path ) ) {
79
      return new ObjectMapper( new YAMLFactory() ).readTree( in );
80
    } catch( final Exception e ) {
81
      // Ensure that a document root node exists by relying on the
82
      // default failure condition when processing. This is required
83
      // because the input stream could not be read.
84
      return new ObjectMapper().createObjectNode();
85
    }
86
  }
87
}
188
A src/main/java/com/keenwrite/definition/yaml/YamlTreeAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.node.ObjectNode;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
33
import com.keenwrite.definition.RootTreeItem;
34
import com.keenwrite.definition.TreeAdapter;
35
import com.keenwrite.definition.DefinitionTreeItem;
36
import javafx.scene.control.TreeItem;
37
import javafx.scene.control.TreeView;
38
39
import java.io.IOException;
40
import java.nio.file.Path;
41
import java.util.Map.Entry;
42
43
/**
44
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
45
 * interface and vice-versa.
46
 */
47
public class YamlTreeAdapter implements TreeAdapter {
48
  private final YamlParser mParser;
49
50
  /**
51
   * Constructs a new instance that will use the given path to read
52
   * the object hierarchy from a data source.
53
   *
54
   * @param path Path to YAML contents to parse.
55
   */
56
  public YamlTreeAdapter( final Path path ) {
57
    mParser = new YamlParser( path );
58
  }
59
60
  @Override
61
  public void export( final TreeItem<String> treeItem, final Path path )
62
      throws IOException {
63
    final YAMLMapper mapper = new YAMLMapper();
64
    final ObjectNode root = mapper.createObjectNode();
65
66
    // Iterate over the root item's children. The root item is used by the
67
    // application to ensure definitions can always be added to a tree, as
68
    // such it is not meant to be exported, only its children.
69
    for( final TreeItem<String> child : treeItem.getChildren() ) {
70
      export( child, root );
71
    }
72
73
    // Writes as UTF8 by default.
74
    mapper.writeValue( path.toFile(), root );
75
  }
76
77
  /**
78
   * Recursive method to generate an object hierarchy that represents the
79
   * given {@link TreeItem} hierarchy.
80
   *
81
   * @param item The {@link TreeItem} to reproduce as an object hierarchy.
82
   * @param node The {@link ObjectNode} to update to reflect the
83
   *             {@link TreeItem} hierarchy.
84
   */
85
  private void export( final TreeItem<String> item, ObjectNode node ) {
86
    final var children = item.getChildren();
87
88
    // If the current item has more than one non-leaf child, it's an
89
    // object node and must become a new nested object.
90
    if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
91
      node = node.putObject( item.getValue() );
92
    }
93
94
    for( final TreeItem<String> child : children ) {
95
      if( child.isLeaf() ) {
96
        node.put( item.getValue(), child.getValue() );
97
      }
98
      else {
99
        export( child, node );
100
      }
101
    }
102
  }
103
104
  /**
105
   * Converts a YAML document to a {@link TreeItem} based on the document
106
   * keys. Only the first document in the stream is adapted.
107
   *
108
   * @param root Root {@link TreeItem} node name.
109
   * @return A {@link TreeItem} populated with all the keys in the YAML
110
   * document.
111
   */
112
  public TreeItem<String> adapt( final String root ) {
113
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
114
    final TreeItem<String> rootItem = createRootTreeItem( root );
115
116
    rootItem.setExpanded( true );
117
    adapt( rootNode, rootItem );
118
    return rootItem;
119
  }
120
121
  /**
122
   * Iterate over a given root node (at any level of the tree) and adapt each
123
   * leaf node.
124
   *
125
   * @param rootNode A JSON node (YAML node) to adapt.
126
   * @param rootItem The tree item to use as the root when processing the node.
127
   */
128
  private void adapt(
129
      final JsonNode rootNode, final TreeItem<String> rootItem ) {
130
    rootNode.fields().forEachRemaining(
131
        ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
132
    );
133
  }
134
135
  /**
136
   * Recursively adapt each rootNode to a corresponding rootItem.
137
   *
138
   * @param rootNode The node to adapt.
139
   * @param rootItem The item to adapt using the node's key.
140
   */
141
  private void adapt(
142
      final Entry<String, JsonNode> rootNode,
143
      final TreeItem<String> rootItem ) {
144
    final JsonNode leafNode = rootNode.getValue();
145
    final String key = rootNode.getKey();
146
    final TreeItem<String> leaf = createTreeItem( key );
147
148
    if( leafNode.isValueNode() ) {
149
      leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
150
    }
151
152
    rootItem.getChildren().add( leaf );
153
154
    if( leafNode.isObject() ) {
155
      adapt( leafNode, leaf );
156
    }
157
  }
158
159
  /**
160
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
161
   *
162
   * @param value The node's value.
163
   * @return A new {@link TreeItem}, never {@code null}.
164
   */
165
  private TreeItem<String> createTreeItem( final String value ) {
166
    return new DefinitionTreeItem<>( value );
167
  }
168
169
  /**
170
   * Creates a new {@link TreeItem} that is intended to be the root-level item
171
   * added to the {@link TreeView}. This allows the root item to be
172
   * distinguished from the other items so that reference keys do not include
173
   * "Definition" as part of their name.
174
   *
175
   * @param value The node's value.
176
   * @return A new {@link TreeItem}, never {@code null}.
177
   */
178
  private TreeItem<String> createRootTreeItem( final String value ) {
179
    return new RootTreeItem<>( value );
180
  }
181
182
  public YamlParser getYamlParser() {
183
    return mParser;
184
  }
185
}
1186
A src/main/java/com/keenwrite/dialogs/AbstractDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.dialogs;
29
30
import static com.keenwrite.Messages.get;
31
import com.keenwrite.service.events.impl.ButtonOrderPane;
32
import static javafx.scene.control.ButtonType.CANCEL;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
37
/**
38
 * Superclass that abstracts common behaviours for all dialogs.
39
 *
40
 * @param <T> The type of dialog to create (usually String).
41
 */
42
public abstract class AbstractDialog<T> extends Dialog<T> {
43
44
  /**
45
   * Ensures that all dialogs can be closed.
46
   *
47
   * @param owner The parent window of this dialog.
48
   * @param title The messages title to display in the title bar.
49
   */
50
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
51
  public AbstractDialog( final Window owner, final String title ) {
52
    setTitle( get( title ) );
53
    setResizable( true );
54
55
    initOwner( owner );
56
    initCloseAction();
57
    initDialogPane();
58
    initDialogButtons();
59
    initComponents();
60
  }
61
62
  /**
63
   * Initialize the component layout.
64
   */
65
  protected abstract void initComponents();
66
67
  /**
68
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
69
   */
70
  protected void initDialogPane() {
71
    setDialogPane( new ButtonOrderPane() );
72
  }
73
  
74
  /**
75
   * Set an OK and CANCEL button on the dialog.
76
   */
77
  protected void initDialogButtons() {
78
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
79
  }
80
81
  /**
82
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
83
   * can always close the window, even if there's an error.
84
   */
85
  protected final void initCloseAction() {
86
    final Window window = getDialogPane().getScene().getWindow();
87
    window.setOnCloseRequest( event -> window.hide() );
88
  }
89
}
190
A src/main/java/com/keenwrite/dialogs/ImageDialog.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.dialogs;
28
29
import static com.keenwrite.Messages.get;
30
import com.keenwrite.controls.BrowseFileButton;
31
import com.keenwrite.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a markdown image.
47
 */
48
public class ImageDialog extends AbstractDialog<String> {
49
50
  private final StringProperty image = new SimpleStringProperty();
51
52
  public ImageDialog( final Window owner, final Path basePath ) {
53
    super(owner, "Dialog.image.title" );
54
    
55
    final DialogPane dialogPane = getDialogPane();
56
    dialogPane.setContent( pane );
57
58
    linkBrowseFileButton.setBasePath( basePath );
59
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
60
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
61
62
    dialogPane.lookupButton( OK ).disableProperty().bind(
63
      urlField.escapedTextProperty().isEmpty()
64
      .or( textField.escapedTextProperty().isEmpty() ) );
65
66
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
69
    previewField.textProperty().bind( image );
70
71
    setResultConverter( dialogButton -> {
72
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
73
      return (data == ButtonData.OK_DONE) ? image.get() : null;
74
    } );
75
76
    Platform.runLater( () -> {
77
      urlField.requestFocus();
78
79
      if( urlField.getText().startsWith( "http://" ) ) {
80
        urlField.selectRange( "http://".length(), urlField.getLength() );
81
      }
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    linkBrowseFileButton = new BrowseFileButton();
92
    Label textLabel = new Label();
93
    textField = new EscapeTextField();
94
    Label titleLabel = new Label();
95
    titleField = new EscapeTextField();
96
    Label previewLabel = new Label();
97
    previewField = new Label();
98
99
    //======== pane ========
100
    {
101
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
102
      pane.setRows( "[][][][]" );
103
104
      //---- urlLabel ----
105
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
106
      pane.add( urlLabel, "cell 0 0" );
107
108
      //---- urlField ----
109
      urlField.setEscapeCharacters( "()" );
110
      urlField.setText( "http://yourlink.com" );
111
      urlField.setPromptText( "http://yourlink.com" );
112
      pane.add( urlField, "cell 1 0" );
113
      pane.add( linkBrowseFileButton, "cell 2 0" );
114
115
      //---- textLabel ----
116
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
117
      pane.add( textLabel, "cell 0 1" );
118
119
      //---- textField ----
120
      textField.setEscapeCharacters( "[]" );
121
      pane.add( textField, "cell 1 1 2 1" );
122
123
      //---- titleLabel ----
124
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
125
      pane.add( titleLabel, "cell 0 2" );
126
      pane.add( titleField, "cell 1 2 2 1" );
127
128
      //---- previewLabel ----
129
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
130
      pane.add( previewLabel, "cell 0 3" );
131
      pane.add( previewField, "cell 1 3 2 1" );
132
    }
133
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
134
  }
135
136
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
137
  private MigPane pane;
138
  private EscapeTextField urlField;
139
  private BrowseFileButton linkBrowseFileButton;
140
  private EscapeTextField textField;
141
  private EscapeTextField titleField;
142
  private Label previewField;
143
	// JFormDesigner - End of variables declaration  //GEN-END:variables
144
}
1145
A src/main/java/com/keenwrite/dialogs/ImageDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "ImageDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "ImageDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
34
				name: "linkBrowseFileButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "javafx.scene.control.Label" ) {
39
				name: "textLabel"
40
				"text": new FormMessage( null, "ImageDialog.textLabel.text" )
41
				auxiliary() {
42
					"JavaCodeGenerator.variableLocal": true
43
				}
44
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
45
				"value": "cell 0 1"
46
			} )
47
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
48
				name: "textField"
49
				"escapeCharacters": "[]"
50
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
51
				"value": "cell 1 1 2 1"
52
			} )
53
			add( new FormComponent( "javafx.scene.control.Label" ) {
54
				name: "titleLabel"
55
				"text": new FormMessage( null, "ImageDialog.titleLabel.text" )
56
				auxiliary() {
57
					"JavaCodeGenerator.variableLocal": true
58
				}
59
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
60
				"value": "cell 0 2"
61
			} )
62
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
63
				name: "titleField"
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 1 2 2 1"
66
			} )
67
			add( new FormComponent( "javafx.scene.control.Label" ) {
68
				name: "previewLabel"
69
				"text": new FormMessage( null, "ImageDialog.previewLabel.text" )
70
				auxiliary() {
71
					"JavaCodeGenerator.variableLocal": true
72
				}
73
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
74
				"value": "cell 0 3"
75
			} )
76
			add( new FormComponent( "javafx.scene.control.Label" ) {
77
				name: "previewField"
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 1 3 2 1"
80
			} )
81
		}, new FormLayoutConstraints( null ) {
82
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
83
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
84
		} )
85
	}
86
}
187
A src/main/java/com/keenwrite/dialogs/LinkDialog.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.dialogs;
29
30
import com.keenwrite.controls.EscapeTextField;
31
import com.keenwrite.editors.markdown.HyperlinkModel;
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.SimpleStringProperty;
35
import javafx.beans.property.StringProperty;
36
import javafx.scene.control.ButtonBar.ButtonData;
37
import javafx.scene.control.DialogPane;
38
import javafx.scene.control.Label;
39
import javafx.stage.Window;
40
import org.tbee.javafx.scene.layout.fxml.MigPane;
41
42
import static com.keenwrite.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
44
45
/**
46
 * Dialog to enter a markdown link.
47
 */
48
public class LinkDialog extends AbstractDialog<String> {
49
50
  private final StringProperty link = new SimpleStringProperty();
51
52
  public LinkDialog(
53
    final Window owner, final HyperlinkModel hyperlink ) {
54
    super( owner, "Dialog.link.title" );
55
56
    final DialogPane dialogPane = getDialogPane();
57
    dialogPane.setContent( pane );
58
59
    dialogPane.lookupButton( OK ).disableProperty().bind(
60
      urlField.escapedTextProperty().isEmpty() );
61
62
    textField.setText( hyperlink.getText() );
63
    urlField.setText( hyperlink.getUrl() );
64
    titleField.setText( hyperlink.getTitle() );
65
66
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
69
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
70
        .otherwise( urlField.escapedTextProperty() ) ) );
71
72
    setResultConverter( dialogButton -> {
73
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
74
      return (data == ButtonData.OK_DONE) ? link.get() : null;
75
    } );
76
77
    Platform.runLater( () -> {
78
      urlField.requestFocus();
79
      urlField.selectRange( 0, urlField.getLength() );
80
    } );
81
  }
82
83
  @Override
84
  protected void initComponents() {
85
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
86
    pane = new MigPane();
87
    Label urlLabel = new Label();
88
    urlField = new EscapeTextField();
89
    Label textLabel = new Label();
90
    textField = new EscapeTextField();
91
    Label titleLabel = new Label();
92
    titleField = new EscapeTextField();
93
94
    //======== pane ========
95
    {
96
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
97
      pane.setRows( "[][][][]" );
98
99
      //---- urlLabel ----
100
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
101
      pane.add( urlLabel, "cell 0 0" );
102
103
      //---- urlField ----
104
      urlField.setEscapeCharacters( "()" );
105
      pane.add( urlField, "cell 1 0" );
106
107
      //---- textLabel ----
108
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
109
      pane.add( textLabel, "cell 0 1" );
110
111
      //---- textField ----
112
      textField.setEscapeCharacters( "[]" );
113
      pane.add( textField, "cell 1 1 3 1" );
114
115
      //---- titleLabel ----
116
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
117
      pane.add( titleLabel, "cell 0 2" );
118
      pane.add( titleField, "cell 1 2 3 1" );
119
    }
120
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
121
  }
122
123
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
124
  private MigPane pane;
125
  private EscapeTextField urlField;
126
  private EscapeTextField textField;
127
  private EscapeTextField titleField;
128
  // JFormDesigner - End of variables declaration  //GEN-END:variables
129
}
1130
A src/main/java/com/keenwrite/dialogs/LinkDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "LinkDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "LinkDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
34
				name: "linkBrowseDirectoyButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
39
				name: "linkBrowseFileButton"
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 3 0"
42
			} )
43
			add( new FormComponent( "javafx.scene.control.Label" ) {
44
				name: "textLabel"
45
				"text": new FormMessage( null, "LinkDialog.textLabel.text" )
46
				auxiliary() {
47
					"JavaCodeGenerator.variableLocal": true
48
				}
49
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
50
				"value": "cell 0 1"
51
			} )
52
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
53
				name: "textField"
54
				"escapeCharacters": "[]"
55
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
56
				"value": "cell 1 1 3 1"
57
			} )
58
			add( new FormComponent( "javafx.scene.control.Label" ) {
59
				name: "titleLabel"
60
				"text": new FormMessage( null, "LinkDialog.titleLabel.text" )
61
				auxiliary() {
62
					"JavaCodeGenerator.variableLocal": true
63
				}
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 0 2"
66
			} )
67
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
68
				name: "titleField"
69
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
70
				"value": "cell 1 2 3 1"
71
			} )
72
			add( new FormComponent( "javafx.scene.control.Label" ) {
73
				name: "previewLabel"
74
				"text": new FormMessage( null, "LinkDialog.previewLabel.text" )
75
				auxiliary() {
76
					"JavaCodeGenerator.variableLocal": true
77
				}
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 0 3"
80
			} )
81
			add( new FormComponent( "javafx.scene.control.Label" ) {
82
				name: "previewField"
83
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
84
				"value": "cell 1 3 3 1"
85
			} )
86
		}, new FormLayoutConstraints( null ) {
87
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
88
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
89
		} )
90
	}
91
}
192
A src/main/java/com/keenwrite/editors/DefinitionDecoratorFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors;
29
30
import com.keenwrite.AbstractFileFactory;
31
import com.keenwrite.sigils.RSigilOperator;
32
import com.keenwrite.sigils.SigilOperator;
33
import com.keenwrite.sigils.YamlSigilOperator;
34
35
import java.nio.file.Path;
36
37
/**
38
 * Responsible for creating a definition name decorator suited to a particular
39
 * file type.
40
 */
41
public class DefinitionDecoratorFactory extends AbstractFileFactory {
42
43
  private DefinitionDecoratorFactory() {
44
  }
45
46
  public static SigilOperator newInstance( final Path path ) {
47
    final var factory = new DefinitionDecoratorFactory();
48
49
    return switch( factory.lookup( path ) ) {
50
      case RMARKDOWN, RXML -> new RSigilOperator();
51
      default -> new YamlSigilOperator();
52
    };
53
  }
54
}
155
A src/main/java/com/keenwrite/editors/DefinitionNameInjector.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors;
29
30
import com.keenwrite.FileEditorTab;
31
import com.keenwrite.definition.DefinitionPane;
32
import com.keenwrite.definition.DefinitionTreeItem;
33
import com.keenwrite.sigils.SigilOperator;
34
import javafx.scene.control.TreeItem;
35
import javafx.scene.input.KeyEvent;
36
import org.fxmisc.richtext.StyledTextArea;
37
38
import java.nio.file.Path;
39
import java.text.BreakIterator;
40
41
import static com.keenwrite.Constants.*;
42
import static com.keenwrite.StatusBarNotifier.alert;
43
import static java.lang.Character.isWhitespace;
44
import static javafx.scene.input.KeyCode.SPACE;
45
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
46
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
47
48
/**
49
 * Provides the logic for injecting variable names within the editor.
50
 */
51
public final class DefinitionNameInjector {
52
53
  /**
54
   * Recipient of name injections.
55
   */
56
  private FileEditorTab mTab;
57
58
  /**
59
   * Initiates double-click events.
60
   */
61
  private final DefinitionPane mDefinitionPane;
62
63
  /**
64
   * Initializes the variable name injector against the given pane.
65
   *
66
   * @param pane The definition panel to listen to for double-click events.
67
   */
68
  public DefinitionNameInjector( final DefinitionPane pane ) {
69
    mDefinitionPane = pane;
70
  }
71
72
  /**
73
   * Trap Control+Space.
74
   *
75
   * @param tab Editor where variable names get injected.
76
   */
77
  public void addListener( final FileEditorTab tab ) {
78
    assert tab != null;
79
    mTab = tab;
80
81
    tab.getEditorPane().addKeyboardListener(
82
        keyPressed( SPACE, CONTROL_DOWN ),
83
        this::autoinsert
84
    );
85
  }
86
87
  /**
88
   * Inserts the currently selected variable from the {@link DefinitionPane}.
89
   */
90
  public void injectSelectedItem() {
91
    final var pane = getDefinitionPane();
92
    final TreeItem<String> item = pane.getSelectedItem();
93
94
    if( item.isLeaf() ) {
95
      final var leaf = pane.findLeafExact( item.getValue() );
96
      final var editor = getEditor();
97
98
      editor.insertText( editor.getCaretPosition(), decorate( leaf ) );
99
    }
100
  }
101
102
  /**
103
   * Pressing Control+SPACE will find a node that matches the current word and
104
   * substitute the definition reference.
105
   */
106
  public void autoinsert() {
107
    final String paragraph = getCaretParagraph();
108
    final int[] bounds = getWordBoundariesAtCaret();
109
110
    try {
111
      if( isEmptyDefinitionPane() ) {
112
        alert( STATUS_DEFINITION_EMPTY );
113
      }
114
      else {
115
        final String word = paragraph.substring( bounds[ 0 ], bounds[ 1 ] );
116
117
        if( word.isBlank() ) {
118
          alert( STATUS_DEFINITION_BLANK );
119
        }
120
        else {
121
          final var leaf = findLeaf( word );
122
123
          if( leaf == null ) {
124
            alert( STATUS_DEFINITION_MISSING, word );
125
          }
126
          else {
127
            replaceText( bounds[ 0 ], bounds[ 1 ], decorate( leaf ) );
128
            expand( leaf );
129
          }
130
        }
131
      }
132
    } catch( final Exception ignored ) {
133
      alert( STATUS_DEFINITION_BLANK );
134
    }
135
  }
136
137
  /**
138
   * Pressing Control+SPACE will find a node that matches the current word and
139
   * substitute the definition reference.
140
   *
141
   * @param e Ignored -- it can only be Control+SPACE.
142
   */
143
  private void autoinsert( final KeyEvent e ) {
144
    autoinsert();
145
  }
146
147
  /**
148
   * Finds the start and end indexes for the word in the current paragraph
149
   * where the caret is located. There are a few different scenarios, where
150
   * the caret can be at: the start, end, or middle of a word; also, the
151
   * caret can be at the end or beginning of a punctuated word; as well, the
152
   * caret could be at the beginning or end of the line or document.
153
   */
154
  private int[] getWordBoundariesAtCaret() {
155
    final var paragraph = getCaretParagraph();
156
    final var length = paragraph.length();
157
    int offset = getCurrentCaretColumn();
158
159
    int began = offset;
160
    int ended = offset;
161
162
    while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
163
      began--;
164
    }
165
166
    while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
167
      ended++;
168
    }
169
170
    final var iterator = BreakIterator.getWordInstance();
171
    iterator.setText( paragraph );
172
173
    while( began < length && iterator.isBoundary( began + 1 ) ) {
174
      began++;
175
    }
176
177
    while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
178
      ended--;
179
    }
180
181
    return new int[]{began, ended};
182
  }
183
184
  /**
185
   * Decorates a {@link TreeItem} using the syntax specific to the type of
186
   * document being edited.
187
   *
188
   * @param leaf The path to the leaf (the definition key) to be decorated.
189
   */
190
  private String decorate( final DefinitionTreeItem<String> leaf ) {
191
    return decorate( leaf.toPath() );
192
  }
193
194
  /**
195
   * Decorates a variable using the syntax specific to the type of document
196
   * being edited.
197
   *
198
   * @param variable The variable to decorate in dot-notation without any
199
   *                 start or end sigils present.
200
   */
201
  private String decorate( final String variable ) {
202
    return getVariableDecorator().apply( variable );
203
  }
204
205
  /**
206
   * Updates the text at the given position within the current paragraph.
207
   *
208
   * @param posBegan The starting index in the paragraph text to replace.
209
   * @param posEnded The ending index in the paragraph text to replace.
210
   * @param text     Overwrite the paragraph substring with this text.
211
   */
212
  private void replaceText(
213
      final int posBegan, final int posEnded, final String text ) {
214
    final int p = getCurrentParagraph();
215
216
    getEditor().replaceText( p, posBegan, p, posEnded, text );
217
  }
218
219
  /**
220
   * Returns the caret's current paragraph position.
221
   *
222
   * @return A number greater than or equal to 0.
223
   */
224
  private int getCurrentParagraph() {
225
    return getEditor().getCurrentParagraph();
226
  }
227
228
  /**
229
   * Returns the text for the paragraph that contains the caret.
230
   *
231
   * @return A non-null string, possibly empty.
232
   */
233
  private String getCaretParagraph() {
234
    return getEditor().getText( getCurrentParagraph() );
235
  }
236
237
  /**
238
   * Returns the caret position within the current paragraph.
239
   *
240
   * @return A value from 0 to the length of the current paragraph.
241
   */
242
  private int getCurrentCaretColumn() {
243
    return getEditor().getCaretColumn();
244
  }
245
246
  /**
247
   * Looks for the given word, matching first by exact, next by a starts-with
248
   * condition with diacritics replaced, then by containment.
249
   *
250
   * @param word The word to match by: exact, at the beginning, or containment.
251
   * @return The matching {@link DefinitionTreeItem} for the given word, or
252
   * {@code null} if none found.
253
   */
254
  @SuppressWarnings("ConstantConditions")
255
  private DefinitionTreeItem<String> findLeaf( final String word ) {
256
    assert word != null;
257
258
    final var pane = getDefinitionPane();
259
    DefinitionTreeItem<String> leaf = null;
260
261
    leaf = leaf == null ? pane.findLeafExact( word ) : leaf;
262
    leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf;
263
    leaf = leaf == null ? pane.findLeafContains( word ) : leaf;
264
    leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf;
265
266
    return leaf;
267
  }
268
269
  /**
270
   * Answers whether there are any definitions in the tree.
271
   *
272
   * @return {@code true} when there are no definitions; {@code false} when
273
   * there's at least one definition.
274
   */
275
  private boolean isEmptyDefinitionPane() {
276
    return getDefinitionPane().isEmpty();
277
  }
278
279
  /**
280
   * Collapses the tree then expands and selects the given node.
281
   *
282
   * @param node The node to expand.
283
   */
284
  private void expand( final TreeItem<String> node ) {
285
    final DefinitionPane pane = getDefinitionPane();
286
    pane.collapse();
287
    pane.expand( node );
288
    pane.select( node );
289
  }
290
291
  /**
292
   * @return A variable decorator that corresponds to the given file type.
293
   */
294
  private SigilOperator getVariableDecorator() {
295
    return DefinitionDecoratorFactory.newInstance( getFilename() );
296
  }
297
298
  private Path getFilename() {
299
    return getFileEditorTab().getPath();
300
  }
301
302
  private EditorPane getEditorPane() {
303
    return getFileEditorTab().getEditorPane();
304
  }
305
306
  private StyledTextArea<?, ?> getEditor() {
307
    return getEditorPane().getEditor();
308
  }
309
310
  public FileEditorTab getFileEditorTab() {
311
    return mTab;
312
  }
313
314
  private DefinitionPane getDefinitionPane() {
315
    return mDefinitionPane;
316
  }
317
}
1318
A src/main/java/com/keenwrite/editors/EditorPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors;
29
30
import com.keenwrite.preferences.UserPreferences;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.ObjectProperty;
33
import javafx.beans.property.SimpleObjectProperty;
34
import javafx.beans.value.ChangeListener;
35
import javafx.event.Event;
36
import javafx.scene.control.ScrollPane;
37
import javafx.scene.layout.Pane;
38
import org.fxmisc.flowless.VirtualizedScrollPane;
39
import org.fxmisc.richtext.StyleClassedTextArea;
40
import org.fxmisc.undo.UndoManager;
41
import org.fxmisc.wellbehaved.event.EventPattern;
42
import org.fxmisc.wellbehaved.event.Nodes;
43
44
import java.nio.file.Path;
45
import java.util.function.Consumer;
46
47
import static com.keenwrite.StatusBarNotifier.clearAlert;
48
import static java.lang.String.format;
49
import static javafx.application.Platform.runLater;
50
import static org.fxmisc.wellbehaved.event.InputMap.consume;
51
52
/**
53
 * Represents common editing features for various types of text editors.
54
 */
55
public class EditorPane extends Pane {
56
57
  /**
58
   * Used when changing the text area font size.
59
   */
60
  private static final String FMT_CSS_FONT_SIZE = "-fx-font-size: %dpt;";
61
62
  private final StyleClassedTextArea mEditor =
63
      new StyleClassedTextArea( false );
64
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
65
      new VirtualizedScrollPane<>( mEditor );
66
  private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>();
67
68
  public EditorPane() {
69
    getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
70
    fontsSizeProperty().addListener(
71
        ( l, o, n ) -> setFontSize( n.intValue() )
72
    );
73
74
    // Clear out any previous alerts after the user has typed. If the problem
75
    // persists, re-rendering the document will re-raise the error. If there
76
    // was no previous error, clearing the alert is essentially a no-op.
77
    mEditor.textProperty().addListener(
78
        ( l, o, n ) -> clearAlert()
79
    );
80
  }
81
82
  @Override
83
  public void requestFocus() {
84
    requestFocus( 3 );
85
  }
86
87
  /**
88
   * There's a race-condition between displaying the {@link EditorPane}
89
   * and giving the {@link #mEditor} focus. Try to focus up to {@code max}
90
   * times before giving up.
91
   *
92
   * @param max The number of attempts to try to request focus.
93
   */
94
  private void requestFocus( final int max ) {
95
    if( max > 0 ) {
96
      runLater(
97
          () -> {
98
            final var editor = getEditor();
99
100
            if( !editor.isFocused() ) {
101
              editor.requestFocus();
102
              requestFocus( max - 1 );
103
            }
104
          }
105
      );
106
    }
107
  }
108
109
  public void undo() {
110
    getUndoManager().undo();
111
  }
112
113
  public void redo() {
114
    getUndoManager().redo();
115
  }
116
117
  /**
118
   * Cuts the actively selected text; if no text is selected, this will cut
119
   * the entire paragraph.
120
   */
121
  public void cut() {
122
    final var editor = getEditor();
123
    final var selected = editor.getSelectedText();
124
125
    if( selected == null || selected.isEmpty() ) {
126
      editor.selectParagraph();
127
    }
128
129
    editor.cut();
130
  }
131
132
  public void copy() {
133
    getEditor().copy();
134
  }
135
136
  public void paste() {
137
    getEditor().paste();
138
  }
139
140
  public void selectAll() {
141
    getEditor().selectAll();
142
  }
143
144
  public UndoManager<?> getUndoManager() {
145
    return getEditor().getUndoManager();
146
  }
147
148
  public String getText() {
149
    return getEditor().getText();
150
  }
151
152
  public void setText( final String text ) {
153
    final var editor = getEditor();
154
    editor.deselect();
155
    editor.replaceText( text );
156
    getUndoManager().mark();
157
  }
158
159
  /**
160
   * Call to hook into changes to the text area.
161
   *
162
   * @param listener Receives editor text change events.
163
   */
164
  public void addTextChangeListener(
165
      final ChangeListener<? super String> listener ) {
166
    getEditor().textProperty().addListener( listener );
167
  }
168
169
  /**
170
   * Notifies observers when the caret changes paragraph.
171
   *
172
   * @param listener Receives change event.
173
   */
174
  public void addCaretParagraphListener(
175
      final ChangeListener<? super Integer> listener ) {
176
    getEditor().currentParagraphProperty().addListener( listener );
177
  }
178
179
  /**
180
   * Notifies observers when the caret changes position.
181
   *
182
   * @param listener Receives change event.
183
   */
184
  public void addCaretPositionListener(
185
      final ChangeListener<? super Integer> listener ) {
186
    getEditor().caretPositionProperty().addListener( listener );
187
  }
188
189
  /**
190
   * This method adds listeners to editor events.
191
   *
192
   * @param <T>      The event type.
193
   * @param <U>      The consumer type for the given event type.
194
   * @param event    The event of interest.
195
   * @param consumer The method to call when the event happens.
196
   */
197
  public <T extends Event, U extends T> void addKeyboardListener(
198
      final EventPattern<? super T, ? extends U> event,
199
      final Consumer<? super U> consumer ) {
200
    Nodes.addInputMap( getEditor(), consume( event, consumer ) );
201
  }
202
203
  /**
204
   * Repositions the cursor and scroll bar to the top of the file.
205
   */
206
  public void scrollToTop() {
207
    getEditor().moveTo( 0 );
208
    getScrollPane().scrollYToPixel( 0 );
209
  }
210
211
  public StyleClassedTextArea getEditor() {
212
    return mEditor;
213
  }
214
215
  /**
216
   * Returns the scroll pane that contains the text area.
217
   *
218
   * @return The scroll pane that contains the content to edit.
219
   */
220
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
221
    return mScrollPane;
222
  }
223
224
  public Path getPath() {
225
    return mPath.get();
226
  }
227
228
  public void setPath( final Path path ) {
229
    mPath.set( path );
230
  }
231
232
  /**
233
   * Sets the font size in points.
234
   *
235
   * @param size The new font size to use for the text editor.
236
   */
237
  private void setFontSize( final int size ) {
238
    mEditor.setStyle( format( FMT_CSS_FONT_SIZE, size ) );
239
  }
240
241
  /**
242
   * Returns the text editor font size property for handling font size change
243
   * events.
244
   */
245
  private IntegerProperty fontsSizeProperty() {
246
    return UserPreferences.getInstance().fontsSizeEditorProperty();
247
  }
248
}
1249
A src/main/java/com/keenwrite/editors/markdown/HyperlinkModel.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
32
/**
33
 * Represents the model for a hyperlink: text, url, and title.
34
 */
35
public class HyperlinkModel {
36
37
  private String text;
38
  private String url;
39
  private String title;
40
41
  /**
42
   * Constructs a new hyperlink model in Markdown format by default with no
43
   * title (i.e., tooltip).
44
   *
45
   * @param text The hyperlink text displayed (e.g., displayed to the user).
46
   * @param url  The destination URL (e.g., when clicked).
47
   */
48
  public HyperlinkModel( final String text, final String url ) {
49
    this( text, url, null );
50
  }
51
52
  /**
53
   * Constructs a new hyperlink model for the given AST link.
54
   *
55
   * @param link A markdown link.
56
   */
57
  public HyperlinkModel( final Link link ) {
58
    this(
59
        link.getText().toString(),
60
        link.getUrl().toString(),
61
        link.getTitle().toString()
62
    );
63
  }
64
65
  /**
66
   * Constructs a new hyperlink model in Markdown format by default.
67
   *
68
   * @param text  The hyperlink text displayed (e.g., displayed to the user).
69
   * @param url   The destination URL (e.g., when clicked).
70
   * @param title The hyperlink title (e.g., shown as a tooltip).
71
   */
72
  public HyperlinkModel( final String text, final String url,
73
                         final String title ) {
74
    setText( text );
75
    setUrl( url );
76
    setTitle( title );
77
  }
78
79
  /**
80
   * Returns the string in Markdown format by default.
81
   *
82
   * @return A markdown version of the hyperlink.
83
   */
84
  @Override
85
  public String toString() {
86
    String format = "%s%s%s";
87
88
    if( hasText() ) {
89
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
90
    }
91
92
    // Becomes ""+URL+"" if no text is set.
93
    // Becomes [TITLE]+(URL)+"" if no title is set.
94
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
95
    return String.format( format, getText(), getUrl(), getTitle() );
96
  }
97
98
  public final void setText( final String text ) {
99
    this.text = nullSafe( text );
100
  }
101
102
  public final void setUrl( final String url ) {
103
    this.url = nullSafe( url );
104
  }
105
106
  public final void setTitle( final String title ) {
107
    this.title = nullSafe( title );
108
  }
109
110
  /**
111
   * Answers whether text has been set for the hyperlink.
112
   *
113
   * @return true This is a text link.
114
   */
115
  public boolean hasText() {
116
    return !getText().isEmpty();
117
  }
118
119
  /**
120
   * Answers whether a title (tooltip) has been set for the hyperlink.
121
   *
122
   * @return true There is a title.
123
   */
124
  public boolean hasTitle() {
125
    return !getTitle().isEmpty();
126
  }
127
128
  public String getText() {
129
    return this.text;
130
  }
131
132
  public String getUrl() {
133
    return this.url;
134
  }
135
136
  public String getTitle() {
137
    return this.title;
138
  }
139
140
  private String nullSafe( final String s ) {
141
    return s == null ? "" : s;
142
  }
143
}
1144
A src/main/java/com/keenwrite/editors/markdown/LinkVisitor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
import com.vladsch.flexmark.util.ast.Node;
32
import com.vladsch.flexmark.util.ast.NodeVisitor;
33
import com.vladsch.flexmark.util.ast.VisitHandler;
34
35
/**
36
 * Responsible for extracting a hyperlink from the document so that the user
37
 * can edit the link within a dialog.
38
 */
39
public class LinkVisitor {
40
41
  private NodeVisitor visitor;
42
  private Link link;
43
  private final int offset;
44
45
  /**
46
   * Creates a hyperlink given an offset into a paragraph and the markdown AST
47
   * link node.
48
   *
49
   * @param index Index into the paragraph that indicates the hyperlink to
50
   *              change.
51
   */
52
  public LinkVisitor( final int index ) {
53
    this.offset = index;
54
  }
55
56
  public Link process( final Node root ) {
57
    getVisitor().visit( root );
58
    return getLink();
59
  }
60
61
  /**
62
   * @param link Not null.
63
   */
64
  private void visit( final Link link ) {
65
    final int began = link.getStartOffset();
66
    final int ended = link.getEndOffset();
67
    final int index = getOffset();
68
69
    if( index >= began && index <= ended ) {
70
      setLink( link );
71
    }
72
  }
73
74
  private synchronized NodeVisitor getVisitor() {
75
    if( this.visitor == null ) {
76
      this.visitor = createVisitor();
77
    }
78
79
    return this.visitor;
80
  }
81
82
  protected NodeVisitor createVisitor() {
83
    return new NodeVisitor(
84
        new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
85
  }
86
87
  private Link getLink() {
88
    return this.link;
89
  }
90
91
  private void setLink( final Link link ) {
92
    this.link = link;
93
  }
94
95
  public int getOffset() {
96
    return this.offset;
97
  }
98
}
199
A src/main/java/com/keenwrite/editors/markdown/MarkdownEditorPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.editors.markdown;
29
30
import com.keenwrite.dialogs.ImageDialog;
31
import com.keenwrite.dialogs.LinkDialog;
32
import com.keenwrite.editors.EditorPane;
33
import com.keenwrite.processors.markdown.BlockExtension;
34
import com.keenwrite.processors.markdown.MarkdownProcessor;
35
import com.vladsch.flexmark.ast.Link;
36
import com.vladsch.flexmark.html.renderer.AttributablePart;
37
import com.vladsch.flexmark.util.ast.Node;
38
import com.vladsch.flexmark.util.html.MutableAttributes;
39
import javafx.scene.control.Dialog;
40
import javafx.scene.control.IndexRange;
41
import javafx.scene.input.KeyCode;
42
import javafx.scene.input.KeyEvent;
43
import javafx.stage.Window;
44
import org.fxmisc.richtext.StyleClassedTextArea;
45
46
import java.nio.file.Path;
47
import java.util.ArrayList;
48
import java.util.List;
49
import java.util.regex.Matcher;
50
import java.util.regex.Pattern;
51
52
import static com.keenwrite.Constants.STYLESHEET_MARKDOWN;
53
import static com.keenwrite.util.Utils.ltrim;
54
import static com.keenwrite.util.Utils.rtrim;
55
import static javafx.scene.input.KeyCode.ENTER;
56
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
57
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
58
59
/**
60
 * Provides the ability to edit a text document.
61
 */
62
public class MarkdownEditorPane extends EditorPane {
63
  private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
64
      "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
65
66
  /**
67
   * Any of these followed by a space and a letter produce a line
68
   * by themselves. The ">" need not be followed by a space.
69
   */
70
  private static final Pattern PATTERN_NEW_LINE = Pattern.compile(
71
      "^>|(((#+)|([*+\\-])|([1-9]\\.))\\s+).+" );
72
73
  public MarkdownEditorPane() {
74
    initEditor();
75
  }
76
77
  private void initEditor() {
78
    final StyleClassedTextArea textArea = getEditor();
79
80
    textArea.setWrapText( true );
81
    textArea.getStyleClass().add( "markdown-editor" );
82
    textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
83
84
    addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
85
    addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut );
86
  }
87
88
  public void insertLink() {
89
    insertObject( createLinkDialog() );
90
  }
91
92
  public void insertImage() {
93
    insertObject( createImageDialog() );
94
  }
95
96
  /**
97
   * Returns the editor's paragraph number that will be close to its HTML
98
   * paragraph ID. Ultimately this solution is flawed because there isn't
99
   * a straightforward correlation between the document being edited and
100
   * what is rendered. XML documents transformed through stylesheets have
101
   * no readily determined correlation. Images, tables, and other
102
   * objects affect the relative location of the current paragraph being
103
   * edited with respect to the preview pane.
104
   * <p>
105
   * See
106
   * {@link BlockExtension.IdAttributeProvider#setAttributes(Node, AttributablePart, MutableAttributes)}}
107
   * for details.
108
   * </p>
109
   * <p>
110
   * Injecting a token into the document, as per a previous version of the
111
   * application, can instruct the preview pane where to shift the viewport.
112
   * </p>
113
   *
114
   * @param paraIndex The paragraph index from the editor pane to scroll to
115
   *                  in the preview pane, which  will be approximated if an
116
   *                  equivalent cannot be found.
117
   * @return A unique identifier that correlates to an equivalent paragraph
118
   * number once the Markdown is rendered into HTML.
119
   */
120
  public int approximateParagraphId( final int paraIndex ) {
121
    final StyleClassedTextArea editor = getEditor();
122
    final List<String> lines = new ArrayList<>( 4096 );
123
124
    int i = 0;
125
    String prevText = "";
126
    boolean withinFencedBlock = false;
127
    boolean withinCodeBlock = false;
128
129
    for( final var p : editor.getParagraphs() ) {
130
      if( i > paraIndex ) {
131
        break;
132
      }
133
134
      final String text = p.getText().replace( '>', ' ' );
135
      if( text.startsWith( "```" ) ) {
136
        if( withinFencedBlock = !withinFencedBlock ) {
137
          lines.add( text );
138
        }
139
      }
140
141
      if( !withinFencedBlock ) {
142
        final boolean foundCodeBlock = text.startsWith( "    " );
143
144
        if( foundCodeBlock && !withinCodeBlock ) {
145
          lines.add( text );
146
          withinCodeBlock = true;
147
        }
148
        else if( !foundCodeBlock ) {
149
          withinCodeBlock = false;
150
        }
151
      }
152
153
      if( !withinFencedBlock && !withinCodeBlock &&
154
          ((!text.isBlank() && prevText.isBlank()) ||
155
              PATTERN_NEW_LINE.matcher( text ).matches()) ) {
156
        lines.add( text );
157
      }
158
159
      prevText = text;
160
      i++;
161
    }
162
163
    // Scrolling index is 1-based.
164
    return Math.max( lines.size() - 1, 0 );
165
  }
166
167
  /**
168
   * Gets the index of the paragraph where the caret is positioned.
169
   *
170
   * @return The paragraph number for the caret.
171
   */
172
  public int getCurrentParagraphIndex() {
173
    return getEditor().getCurrentParagraph();
174
  }
175
176
  /**
177
   * @param leading  Characters to insert at the beginning of the current
178
   *                 selection (or paragraph).
179
   * @param trailing Characters to insert at the end of the current selection
180
   *                 (or paragraph).
181
   */
182
  public void surroundSelection( final String leading, final String trailing ) {
183
    surroundSelection( leading, trailing, null );
184
  }
185
186
  /**
187
   * @param leading  Characters to insert at the beginning of the current
188
   *                 selection (or paragraph).
189
   * @param trailing Characters to insert at the end of the current selection
190
   *                 (or paragraph).
191
   * @param hint     Instructional text inserted within the leading and
192
   *                 trailing characters, provided no text is selected.
193
   */
194
  public void surroundSelection(
195
      String leading, String trailing, final String hint ) {
196
    final StyleClassedTextArea textArea = getEditor();
197
198
    // Note: not using textArea.insertText() to insert leading and trailing
199
    // because this would add two changes to undo history
200
    final IndexRange selection = textArea.getSelection();
201
    int start = selection.getStart();
202
    int end = selection.getEnd();
203
204
    final String selectedText = textArea.getSelectedText();
205
206
    String trimmedText = selectedText.trim();
207
    if( trimmedText.length() < selectedText.length() ) {
208
      start += selectedText.indexOf( trimmedText );
209
      end = start + trimmedText.length();
210
    }
211
212
    // remove leading whitespaces from leading text if selection starts at zero
213
    if( start == 0 ) {
214
      leading = ltrim( leading );
215
    }
216
217
    // remove trailing whitespaces from trailing text if selection ends at
218
    // text end
219
    if( end == textArea.getLength() ) {
220
      trailing = rtrim( trailing );
221
    }
222
223
    // remove leading line separators from leading text
224
    // if there are line separators before the selected text
225
    if( leading.startsWith( "\n" ) ) {
226
      for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
227
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
228
          break;
229
        }
230
231
        leading = leading.substring( 1 );
232
      }
233
    }
234
235
    // remove trailing line separators from trailing or leading text
236
    // if there are line separators after the selected text
237
    final boolean trailingIsEmpty = trailing.isEmpty();
238
    String str = trailingIsEmpty ? leading : trailing;
239
240
    if( str.endsWith( "\n" ) ) {
241
      final int length = textArea.getLength();
242
243
      for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
244
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
245
          break;
246
        }
247
248
        str = str.substring( 0, str.length() - 1 );
249
      }
250
251
      if( trailingIsEmpty ) {
252
        leading = str;
253
      }
254
      else {
255
        trailing = str;
256
      }
257
    }
258
259
    int selStart = start + leading.length();
260
    int selEnd = end + leading.length();
261
262
    // insert hint text if selection is empty
263
    if( hint != null && trimmedText.isEmpty() ) {
264
      trimmedText = hint;
265
      selEnd = selStart + hint.length();
266
    }
267
268
    // prevent undo merging with previous text entered by user
269
    getUndoManager().preventMerge();
270
271
    // replace text and update selection
272
    textArea.replaceText( start, end, leading + trimmedText + trailing );
273
    textArea.selectRange( selStart, selEnd );
274
  }
275
276
  private void enterPressed( final KeyEvent e ) {
277
    final StyleClassedTextArea textArea = getEditor();
278
    final String currentLine =
279
        textArea.getText( textArea.getCurrentParagraph() );
280
    final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
281
282
    String newText = "\n";
283
284
    if( matcher.matches() ) {
285
      if( !matcher.group( 2 ).isEmpty() ) {
286
        // indent new line with same whitespace characters and list markers
287
        // as current line
288
        newText = newText.concat( matcher.group( 1 ) );
289
      }
290
      else {
291
        // current line contains only whitespace characters and list markers
292
        // --> empty current line
293
        final int caretPosition = textArea.getCaretPosition();
294
        textArea.selectRange( caretPosition - currentLine.length(),
295
                              caretPosition );
296
      }
297
    }
298
299
    textArea.replaceSelection( newText );
300
301
    // Ensure that the window scrolls when Enter is pressed at the bottom of
302
    // the pane.
303
    textArea.requestFollowCaret();
304
  }
305
306
  private void cut( final KeyEvent event ) {
307
    super.cut();
308
  }
309
310
  /**
311
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
312
   * the markdown AST.
313
   *
314
   * @return An instance containing the link URL and display text.
315
   */
316
  private HyperlinkModel getHyperlink() {
317
    final StyleClassedTextArea textArea = getEditor();
318
    final String selectedText = textArea.getSelectedText();
319
320
    // Get the current paragraph, convert to Markdown nodes.
321
    final MarkdownProcessor mp = new MarkdownProcessor( null );
322
    final int p = textArea.getCurrentParagraph();
323
    final String paragraph = textArea.getText( p );
324
    final Node node = mp.toNode( paragraph );
325
    final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
326
    final Link link = visitor.process( node );
327
328
    if( link != null ) {
329
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
330
    }
331
332
    return createHyperlinkModel(
333
        link, selectedText, "https://localhost"
334
    );
335
  }
336
337
  @SuppressWarnings("SameParameterValue")
338
  private HyperlinkModel createHyperlinkModel(
339
      final Link link, final String selection, final String url ) {
340
341
    return link == null
342
        ? new HyperlinkModel( selection, url )
343
        : new HyperlinkModel( link );
344
  }
345
346
  private Path getParentPath() {
347
    final Path path = getPath();
348
    return (path != null) ? path.getParent() : null;
349
  }
350
351
  private Dialog<String> createLinkDialog() {
352
    return new LinkDialog( getWindow(), getHyperlink() );
353
  }
354
355
  private Dialog<String> createImageDialog() {
356
    return new ImageDialog( getWindow(), getParentPath() );
357
  }
358
359
  private void insertObject( final Dialog<String> dialog ) {
360
    dialog.showAndWait().ifPresent(
361
        result -> getEditor().replaceSelection( result )
362
    );
363
  }
364
365
  private Window getWindow() {
366
    return getScrollPane().getScene().getWindow();
367
  }
368
}
1369
A src/main/java/com/keenwrite/exceptions/MissingFileException.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.exceptions;
29
30
import java.io.FileNotFoundException;
31
32
import static com.keenwrite.Messages.get;
33
34
/**
35
 * Responsible for informing the user when a file cannot be found.
36
 * This avoids duplicating the error message prefix.
37
 */
38
public class MissingFileException extends FileNotFoundException {
39
  /**
40
   * Constructs a new {@link MissingFileException} using the given path.
41
   *
42
   * @param uri The path to the file resource that could not be found.
43
   */
44
  public MissingFileException( final String uri ) {
45
    super( get( "Main.status.error.file.missing", uri ) );
46
  }
47
}
148
A src/main/java/com/keenwrite/predicates/PredicateFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.predicates;
29
30
import java.io.File;
31
import java.util.Collection;
32
import java.util.function.Predicate;
33
34
import static java.lang.String.join;
35
import static java.nio.file.FileSystems.getDefault;
36
37
/**
38
 * Provides a number of simple {@link Predicate} instances for various types
39
 * of string comparisons, including basic strings and file name strings.
40
 */
41
public class PredicateFactory {
42
  /**
43
   * Creates an instance of {@link Predicate} that matches a globbed file
44
   * name pattern.
45
   *
46
   * @param pattern The file name pattern to match.
47
   * @return A {@link Predicate} that can answer whether a given file name
48
   * matches the given glob pattern.
49
   */
50
  public static Predicate<File> createFileTypePredicate(
51
      final String pattern ) {
52
    final var matcher = getDefault().getPathMatcher(
53
        "glob:**{" + pattern + "}"
54
    );
55
56
    return file -> matcher.matches( file.toPath() );
57
  }
58
59
  /**
60
   * Creates an instance of {@link Predicate} that matches any file name from
61
   * a {@link Collection} of file name patterns. The given patterns are joined
62
   * with commas into a single comma-separated list.
63
   *
64
   * @param patterns The file name patterns to be matched.
65
   * @return A {@link Predicate} that can answer whether a given file name
66
   * matches the given glob patterns.
67
   */
68
  public static Predicate<File> createFileTypePredicate(
69
      final Collection<String> patterns ) {
70
    return createFileTypePredicate( join( ",", patterns ) );
71
  }
72
73
  /**
74
   * Creates an instance of {@link Predicate} that compares whether the given
75
   * {@code reference} string is contained by the comparator. Comparison is
76
   * case-insensitive. The test will also pass if the comparate is empty.
77
   *
78
   * @param comparator The string to check as being contained.
79
   * @return A {@link Predicate} that can answer whether the given string
80
   * is contained within the comparator, or the comparate is empty.
81
   */
82
  public static Predicate<String> createStringContainsPredicate(
83
      final String comparator ) {
84
    return comparate -> comparate.isEmpty() ||
85
        comparate.toLowerCase().contains( comparator.toLowerCase() );
86
  }
187
88
  /**
89
   * Creates an instance of {@link Predicate} that compares whether the given
90
   * {@code reference} string is starts with the comparator. Comparison is
91
   * case-insensitive.
92
   *
93
   * @param comparator The string to check as being contained.
94
   * @return A {@link Predicate} that can answer whether the given string
95
   * is contained within the comparator.
96
   */
97
  public static Predicate<String> createStringStartsPredicate(
98
      final String comparator ) {
99
    return comparate ->
100
        comparate.toLowerCase().startsWith( comparator.toLowerCase() );
101
  }
102
}
A src/main/java/com/keenwrite/preferences/FilePreferences.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preferences;
29
30
import java.io.File;
31
import java.io.FileInputStream;
32
import java.io.FileOutputStream;
33
import java.util.*;
34
import java.util.prefs.AbstractPreferences;
35
import java.util.prefs.BackingStoreException;
36
37
import static com.keenwrite.StatusBarNotifier.alert;
38
39
/**
40
 * Preferences implementation that stores to a user-defined file. Local file
41
 * storage is preferred over a certain operating system's monolithic trash heap
42
 * called a registry. When the OS is locked down, the default Preferences
43
 * implementation will try to write to the registry and fail due to permissions
44
 * problems. This class sidesteps the issue entirely by writing to the user's
45
 * home directory, where permissions should be a bit more lax.
46
 */
47
public class FilePreferences extends AbstractPreferences {
48
49
  private final Map<String, String> mRoot = new TreeMap<>();
50
  private final Map<String, FilePreferences> mChildren = new TreeMap<>();
51
  private boolean mRemoved;
52
53
  private final Object mMutex = new Object();
54
55
  public FilePreferences(
56
      final AbstractPreferences parent, final String name ) {
57
    super( parent, name );
58
59
    try {
60
      sync();
61
    } catch( final BackingStoreException ex ) {
62
      alert( ex );
63
    }
64
  }
65
66
  @Override
67
  protected void putSpi( final String key, final String value ) {
68
    synchronized( mMutex ) {
69
      mRoot.put( key, value );
70
    }
71
72
    try {
73
      flush();
74
    } catch( final BackingStoreException ex ) {
75
      alert( ex );
76
    }
77
  }
78
79
  @Override
80
  protected String getSpi( final String key ) {
81
    synchronized( mMutex ) {
82
      return mRoot.get( key );
83
    }
84
  }
85
86
  @Override
87
  protected void removeSpi( final String key ) {
88
    synchronized( mMutex ) {
89
      mRoot.remove( key );
90
    }
91
92
    try {
93
      flush();
94
    } catch( final BackingStoreException ex ) {
95
      alert( ex );
96
    }
97
  }
98
99
  @Override
100
  protected void removeNodeSpi() throws BackingStoreException {
101
    mRemoved = true;
102
    flush();
103
  }
104
105
  @Override
106
  protected String[] keysSpi() {
107
    synchronized( mMutex ) {
108
      return mRoot.keySet().toArray( new String[ 0 ] );
109
    }
110
  }
111
112
  @Override
113
  protected String[] childrenNamesSpi() {
114
    return mChildren.keySet().toArray( new String[ 0 ] );
115
  }
116
117
  @Override
118
  protected FilePreferences childSpi( final String name ) {
119
    FilePreferences child = mChildren.get( name );
120
121
    if( child == null || child.isRemoved() ) {
122
      child = new FilePreferences( this, name );
123
      mChildren.put( name, child );
124
    }
125
126
    return child;
127
  }
128
129
  @Override
130
  protected void syncSpi() {
131
    if( isRemoved() ) {
132
      return;
133
    }
134
135
    final File file = FilePreferencesFactory.getPreferencesFile();
136
137
    if( !file.exists() ) {
138
      return;
139
    }
140
141
    synchronized( mMutex ) {
142
      final Properties p = new Properties();
143
144
      try( final var inputStream = new FileInputStream( file ) ) {
145
        p.load( inputStream );
146
147
        final String path = getPath();
148
        final Enumeration<?> propertyNames = p.propertyNames();
149
150
        while( propertyNames.hasMoreElements() ) {
151
          final String propKey = (String) propertyNames.nextElement();
152
153
          if( propKey.startsWith( path ) ) {
154
            final String subKey = propKey.substring( path.length() );
155
156
            // Only load immediate descendants
157
            if( subKey.indexOf( '.' ) == -1 ) {
158
              mRoot.put( subKey, p.getProperty( propKey ) );
159
            }
160
          }
161
        }
162
      } catch( final Exception ex ) {
163
        alert( ex );
164
      }
165
    }
166
  }
167
168
  private String getPath() {
169
    final FilePreferences parent = (FilePreferences) parent();
170
171
    return parent == null ? "" : parent.getPath() + name() + '.';
172
  }
173
174
  @Override
175
  protected void flushSpi() {
176
    final File file = FilePreferencesFactory.getPreferencesFile();
177
178
    synchronized( mMutex ) {
179
      final Properties p = new Properties();
180
181
      try {
182
        final String path = getPath();
183
184
        if( file.exists() ) {
185
          try( final var fis = new FileInputStream( file ) ) {
186
            p.load( fis );
187
          }
188
189
          final List<String> toRemove = new ArrayList<>();
190
191
          // Make a list of all direct children of this node to be removed
192
          final Enumeration<?> propertyNames = p.propertyNames();
193
194
          while( propertyNames.hasMoreElements() ) {
195
            final String propKey = (String) propertyNames.nextElement();
196
            if( propKey.startsWith( path ) ) {
197
              final String subKey = propKey.substring( path.length() );
198
199
              // Only do immediate descendants
200
              if( subKey.indexOf( '.' ) == -1 ) {
201
                toRemove.add( propKey );
202
              }
203
            }
204
          }
205
206
          // Remove them now that the enumeration is done with
207
          for( final String propKey : toRemove ) {
208
            p.remove( propKey );
209
          }
210
        }
211
212
        // If this node hasn't been removed, add back in any values
213
        if( !mRemoved ) {
214
          for( final String s : mRoot.keySet() ) {
215
            p.setProperty( path + s, mRoot.get( s ) );
216
          }
217
        }
218
219
        try( final var fos = new FileOutputStream( file ) ) {
220
          p.store( fos, "FilePreferences" );
221
        }
222
      } catch( final Exception ex ) {
223
        alert( ex );
224
      }
225
    }
226
  }
227
}
1228
A src/main/java/com/keenwrite/preferences/FilePreferencesFactory.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preferences;
29
30
import java.io.File;
31
import java.util.prefs.Preferences;
32
import java.util.prefs.PreferencesFactory;
33
34
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
35
import static java.io.File.separator;
36
import static java.lang.System.getProperty;
37
38
/**
39
 * PreferencesFactory implementation that stores the preferences in a
40
 * user-defined file. Usage:
41
 * <pre>
42
 * System.setProperty( "java.util.prefs.PreferencesFactory",
43
 * FilePreferencesFactory.class.getName() );
44
 * </pre>
45
 */
46
public class FilePreferencesFactory implements PreferencesFactory {
47
48
  private static File sPreferencesFile;
49
  private Preferences rootPreferences;
50
51
  @Override
52
  public Preferences systemRoot() {
53
    return userRoot();
54
  }
55
56
  @Override
57
  public Preferences userRoot() {
58
    final var prefs = rootPreferences;
59
60
    if( prefs == null ) {
61
      rootPreferences = new FilePreferences( null, "" );
62
    }
63
64
    return rootPreferences;
65
  }
66
67
  public static File getPreferencesFile() {
68
    final var prefs = sPreferencesFile;
69
70
    if( prefs == null ) {
71
      sPreferencesFile = new File( getPreferencesFilename() ).getAbsoluteFile();
72
    }
73
74
    return sPreferencesFile;
75
  }
76
77
  public static String getPreferencesFilename() {
78
    final var filename = getProperty( "application.name", APP_TITLE_LOWERCASE );
79
    return getProperty( "user.home" ) + separator + '.' + filename;
80
  }
81
}
182
A src/main/java/com/keenwrite/preferences/UserPreferences.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preferences;
29
30
import com.dlsc.formsfx.model.structure.StringField;
31
import com.dlsc.preferencesfx.PreferencesFx;
32
import com.dlsc.preferencesfx.PreferencesFxEvent;
33
import com.dlsc.preferencesfx.model.Category;
34
import com.dlsc.preferencesfx.model.Group;
35
import com.dlsc.preferencesfx.model.Setting;
36
import javafx.beans.property.*;
37
import javafx.event.EventHandler;
38
import javafx.scene.Node;
39
import javafx.scene.control.Label;
40
41
import java.io.File;
42
import java.nio.file.Path;
43
44
import static com.keenwrite.Constants.*;
45
import static com.keenwrite.Messages.get;
46
47
/**
48
 * Responsible for user preferences that can be changed from the GUI. The
49
 * settings are displayed and persisted using {@link PreferencesFx}.
50
 */
51
public class UserPreferences {
52
  /**
53
   * Implementation of the  initialization-on-demand holder design pattern,
54
   * an for a lazy-loaded singleton. In all versions of Java, the idiom enables
55
   * a safe, highly concurrent lazy initialization of static fields with good
56
   * performance. The implementation relies upon the initialization phase of
57
   * execution within the Java Virtual Machine (JVM) as specified by the Java
58
   * Language Specification. When the class {@link UserPreferencesContainer}
59
   * is loaded, its initialization completes trivially because there are no
60
   * static variables to initialize.
61
   * <p>
62
   * The static class definition {@link UserPreferencesContainer} within the
63
   * {@link UserPreferences} is not initialized until such time that
64
   * {@link UserPreferencesContainer} must be executed. The static
65
   * {@link UserPreferencesContainer} class executes when
66
   * {@link #getInstance} is called. The first call will trigger loading and
67
   * initialization of the {@link UserPreferencesContainer} thereby
68
   * instantiating the {@link #INSTANCE}.
69
   * </p>
70
   * <p>
71
   * This indirection is necessary because the {@link UserPreferences} class
72
   * references {@link PreferencesFx}, which must not be instantiated until the
73
   * UI is ready.
74
   * </p>
75
   */
76
  private static class UserPreferencesContainer {
77
    private static final UserPreferences INSTANCE = new UserPreferences();
78
  }
79
80
  public static UserPreferences getInstance() {
81
    return UserPreferencesContainer.INSTANCE;
82
  }
83
84
  private final PreferencesFx mPreferencesFx;
85
86
  private final ObjectProperty<File> mPropRDirectory;
87
  private final StringProperty mPropRScript;
88
  private final ObjectProperty<File> mPropImagesDirectory;
89
  private final StringProperty mPropImagesOrder;
90
  private final ObjectProperty<File> mPropDefinitionPath;
91
  private final StringProperty mRDelimiterBegan;
92
  private final StringProperty mRDelimiterEnded;
93
  private final StringProperty mDefDelimiterBegan;
94
  private final StringProperty mDefDelimiterEnded;
95
  private final IntegerProperty mPropFontsSizeEditor;
96
97
  private UserPreferences() {
98
    mPropRDirectory = simpleFile( USER_DIRECTORY );
99
    mPropRScript = new SimpleStringProperty( "" );
100
101
    mPropImagesDirectory = simpleFile( USER_DIRECTORY );
102
    mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT );
103
104
    mPropDefinitionPath = simpleFile(
105
        getSetting( "file.definition.default", DEFINITION_NAME )
106
    );
107
108
    mDefDelimiterBegan = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT );
109
    mDefDelimiterEnded = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT );
110
111
    mRDelimiterBegan = new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT );
112
    mRDelimiterEnded = new SimpleStringProperty( R_DELIM_ENDED_DEFAULT );
113
114
    mPropFontsSizeEditor = new SimpleIntegerProperty( (int) FONT_SIZE_EDITOR );
115
116
    // All properties must be initialized before creating the dialog.
117
    mPreferencesFx = createPreferencesFx();
118
  }
119
120
  /**
121
   * Display the user preferences settings dialog (non-modal).
122
   */
123
  public void show() {
124
    getPreferencesFx().show( false );
125
  }
126
127
  /**
128
   * Call to persist the settings. Strictly speaking, this could watch on
129
   * all values for external changes then save automatically.
130
   */
131
  public void save() {
132
    getPreferencesFx().saveSettings();
133
  }
134
135
  /**
136
   * Creates the preferences dialog.
137
   * <p>
138
   * TODO: Make this dynamic by iterating over all "Preferences.*" values
139
   * that follow a particular naming pattern.
140
   * </p>
141
   *
142
   * @return A new instance of preferences for users to edit.
143
   */
144
  @SuppressWarnings("unchecked")
145
  private PreferencesFx createPreferencesFx() {
146
    final Setting<StringField, StringProperty> scriptSetting =
147
        Setting.of( "Script", mPropRScript );
148
    final StringField field = scriptSetting.getElement();
149
    field.multiline( true );
150
151
    return PreferencesFx.of(
152
        UserPreferences.class,
153
        Category.of(
154
            get( "Preferences.r" ),
155
            Group.of(
156
                get( "Preferences.r.directory" ),
157
                Setting.of( label( "Preferences.r.directory.desc", false ) ),
158
                Setting.of( "Directory", mPropRDirectory, true )
159
            ),
160
            Group.of(
161
                get( "Preferences.r.script" ),
162
                Setting.of( label( "Preferences.r.script.desc" ) ),
163
                scriptSetting
164
            ),
165
            Group.of(
166
                get( "Preferences.r.delimiter.began" ),
167
                Setting.of( label( "Preferences.r.delimiter.began.desc" ) ),
168
                Setting.of( "Opening", mRDelimiterBegan )
169
            ),
170
            Group.of(
171
                get( "Preferences.r.delimiter.ended" ),
172
                Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ),
173
                Setting.of( "Closing", mRDelimiterEnded )
174
            )
175
        ),
176
        Category.of(
177
            get( "Preferences.images" ),
178
            Group.of(
179
                get( "Preferences.images.directory" ),
180
                Setting.of( label( "Preferences.images.directory.desc" ) ),
181
                Setting.of( "Directory", mPropImagesDirectory, true )
182
            ),
183
            Group.of(
184
                get( "Preferences.images.suffixes" ),
185
                Setting.of( label( "Preferences.images.suffixes.desc" ) ),
186
                Setting.of( "Extensions", mPropImagesOrder )
187
            )
188
        ),
189
        Category.of(
190
            get( "Preferences.definitions" ),
191
            Group.of(
192
                get( "Preferences.definitions.path" ),
193
                Setting.of( label( "Preferences.definitions.path.desc" ) ),
194
                Setting.of( "Path", mPropDefinitionPath, false )
195
            ),
196
            Group.of(
197
                get( "Preferences.definitions.delimiter.began" ),
198
                Setting.of( label(
199
                    "Preferences.definitions.delimiter.began.desc" ) ),
200
                Setting.of( "Opening", mDefDelimiterBegan )
201
            ),
202
            Group.of(
203
                get( "Preferences.definitions.delimiter.ended" ),
204
                Setting.of( label(
205
                    "Preferences.definitions.delimiter.ended.desc" ) ),
206
                Setting.of( "Closing", mDefDelimiterEnded )
207
            )
208
        ),
209
        Category.of(
210
            get( "Preferences.fonts" ),
211
            Group.of(
212
                get( "Preferences.fonts.size_editor" ),
213
                Setting.of( label( "Preferences.fonts.size_editor.desc" ) ),
214
                Setting.of( "Points", mPropFontsSizeEditor )
215
            )
216
        )
217
    ).instantPersistent( false )
218
                        .dialogIcon( ICON_DIALOG );
219
  }
220
221
  /**
222
   * Wraps a {@link File} inside a {@link SimpleObjectProperty}.
223
   *
224
   * @param path The file name to use when constructing the {@link File}.
225
   * @return A new {@link SimpleObjectProperty} instance with a {@link File}
226
   * that references the given {@code path}.
227
   */
228
  private SimpleObjectProperty<File> simpleFile( final String path ) {
229
    return new SimpleObjectProperty<>( new File( path ) );
230
  }
231
232
  /**
233
   * Creates a label for the given key after interpolating its value.
234
   *
235
   * @param key The key to find in the resource bundle.
236
   * @return The value of the key as a label.
237
   */
238
  private Node label( final String key ) {
239
    return new Label( get( key, true ) );
240
  }
241
242
  /**
243
   * Creates a label for the given key.
244
   *
245
   * @param key         The key to find in the resource bundle.
246
   * @param interpolate {@code true} means to interpolate the value.
247
   * @return The value of the key, interpolated if {@code interpolate} is
248
   * {@code true}.
249
   */
250
  @SuppressWarnings("SameParameterValue")
251
  private Node label( final String key, final boolean interpolate ) {
252
    return new Label( get( key, interpolate ) );
253
  }
254
255
  /**
256
   * Delegates to the {@link PreferencesFx} event handler for monitoring
257
   * save events.
258
   *
259
   * @param eventHandler The handler to call when the preferences are saved.
260
   */
261
  public void addSaveEventHandler(
262
      final EventHandler<? super PreferencesFxEvent> eventHandler ) {
263
    final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
264
    getPreferencesFx().addEventHandler( eventType, eventHandler );
265
  }
266
267
  /**
268
   * Returns the value for a key from the settings properties file.
269
   *
270
   * @param key   Key within the settings properties file to find.
271
   * @param value Default value to return if the key is not found.
272
   * @return The value for the given key from the settings file, or the
273
   * given {@code value} if no key found.
274
   */
275
  @SuppressWarnings("SameParameterValue")
276
  private String getSetting( final String key, final String value ) {
277
    return SETTINGS.getSetting( key, value );
278
  }
279
280
  public ObjectProperty<File> definitionPathProperty() {
281
    return mPropDefinitionPath;
282
  }
283
284
  public Path getDefinitionPath() {
285
    return definitionPathProperty().getValue().toPath();
286
  }
287
288
  private StringProperty defDelimiterBegan() {
289
    return mDefDelimiterBegan;
290
  }
291
292
  public String getDefDelimiterBegan() {
293
    return defDelimiterBegan().get();
294
  }
295
296
  private StringProperty defDelimiterEnded() {
297
    return mDefDelimiterEnded;
298
  }
299
300
  public String getDefDelimiterEnded() {
301
    return defDelimiterEnded().get();
302
  }
303
304
  public ObjectProperty<File> rDirectoryProperty() {
305
    return mPropRDirectory;
306
  }
307
308
  public File getRDirectory() {
309
    return rDirectoryProperty().getValue();
310
  }
311
312
  public StringProperty rScriptProperty() {
313
    return mPropRScript;
314
  }
315
316
  public String getRScript() {
317
    return rScriptProperty().getValue();
318
  }
319
320
  private StringProperty rDelimiterBegan() {
321
    return mRDelimiterBegan;
322
  }
323
324
  public String getRDelimiterBegan() {
325
    return rDelimiterBegan().get();
326
  }
327
328
  private StringProperty rDelimiterEnded() {
329
    return mRDelimiterEnded;
330
  }
331
332
  public String getRDelimiterEnded() {
333
    return rDelimiterEnded().get();
334
  }
335
336
  private ObjectProperty<File> imagesDirectoryProperty() {
337
    return mPropImagesDirectory;
338
  }
339
340
  public File getImagesDirectory() {
341
    return imagesDirectoryProperty().getValue();
342
  }
343
344
  private StringProperty imagesOrderProperty() {
345
    return mPropImagesOrder;
346
  }
347
348
  public String getImagesOrder() {
349
    return imagesOrderProperty().getValue();
350
  }
351
352
  public IntegerProperty fontsSizeEditorProperty() {
353
    return mPropFontsSizeEditor;
354
  }
355
356
  /**
357
   * Returns the preferred font size of the text editor.
358
   *
359
   * @return A non-negative integer, in points.
360
   */
361
  public int getFontsSizeEditor() {
362
    return mPropFontsSizeEditor.intValue();
363
  }
364
365
  private PreferencesFx getPreferencesFx() {
366
    return mPreferencesFx;
367
  }
368
}
1369
A src/main/java/com/keenwrite/preview/ChainedReplacedElementFactory.java
1
/*
2
 * Copyright 2006 Patrick Wright
3
 * Copyright 2007 Wisconsin Court System
4
 * Copyright 2020 White Magic Software, Ltd.
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU Lesser General Public License
8
 * as published by the Free Software Foundation; either version 2.1
9
 * of the License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19
 */
20
package com.keenwrite.preview;
21
22
import com.keenwrite.adapters.ReplacedElementAdapter;
23
import org.w3c.dom.Element;
24
import org.xhtmlrenderer.extend.ReplacedElement;
25
import org.xhtmlrenderer.extend.ReplacedElementFactory;
26
import org.xhtmlrenderer.extend.UserAgentCallback;
27
import org.xhtmlrenderer.layout.LayoutContext;
28
import org.xhtmlrenderer.render.BlockBox;
29
30
import java.util.HashSet;
31
import java.util.Set;
32
33
public class ChainedReplacedElementFactory extends ReplacedElementAdapter {
34
  private final Set<ReplacedElementFactory> mFactoryList = new HashSet<>();
35
36
  @Override
37
  public ReplacedElement createReplacedElement(
38
      final LayoutContext c,
39
      final BlockBox box,
40
      final UserAgentCallback uac,
41
      final int cssWidth,
42
      final int cssHeight ) {
43
    for( final var f : mFactoryList ) {
44
      final var r = f.createReplacedElement(
45
          c, box, uac, cssWidth, cssHeight );
46
47
      if( r != null ) {
48
        return r;
49
      }
50
    }
51
52
    return null;
53
  }
54
55
  @Override
56
  public void reset() {
57
    for( final var factory : mFactoryList ) {
58
      factory.reset();
59
    }
60
  }
61
62
  @Override
63
  public void remove( final Element element ) {
64
    for( final var factory : mFactoryList ) {
65
      factory.remove( element );
66
    }
67
  }
68
69
  public void addFactory( final ReplacedElementFactory factory ) {
70
    mFactoryList.add( factory );
71
  }
72
}
173
A src/main/java/com/keenwrite/preview/CustomImageLoader.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preview;
29
30
import com.keenwrite.exceptions.MissingFileException;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.SimpleIntegerProperty;
33
import org.xhtmlrenderer.extend.FSImage;
34
import org.xhtmlrenderer.resource.ImageResource;
35
import org.xhtmlrenderer.swing.ImageResourceLoader;
36
37
import javax.imageio.ImageIO;
38
import java.net.URI;
39
import java.net.URL;
40
import java.nio.file.Paths;
41
42
import static com.keenwrite.StatusBarNotifier.alert;
43
import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
44
import static com.keenwrite.util.ProtocolResolver.getProtocol;
45
import static java.lang.String.valueOf;
46
import static java.nio.file.Files.exists;
47
import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
48
49
/**
50
 * Responsible for loading images. If the image cannot be found, a placeholder
51
 * is used instead.
52
 */
53
public class CustomImageLoader extends ImageResourceLoader {
54
  /**
55
   * Placeholder that's displayed when image cannot be found.
56
   */
57
  private FSImage mBrokenImage;
58
59
  private final IntegerProperty mWidthProperty = new SimpleIntegerProperty();
60
61
  /**
62
   * Gets an {@link IntegerProperty} that represents the maximum width an
63
   * image should be scaled.
64
   *
65
   * @return The maximum width for an image.
66
   */
67
  public IntegerProperty widthProperty() {
68
    return mWidthProperty;
69
  }
70
71
  /**
72
   * Gets an image resolved from the given URI. If the image cannot be found,
73
   * this will return a custom placeholder image indicating the reference
74
   * is broken.
75
   *
76
   * @param uri    Path to the image resource to load.
77
   * @param width  Ignored.
78
   * @param height Ignored.
79
   * @return The scaled image, or a placeholder image if the URI's content
80
   * could not be retrieved.
81
   */
82
  @Override
83
  public synchronized ImageResource get(
84
      final String uri, final int width, final int height ) {
85
    assert uri != null;
86
    assert width >= 0;
87
    assert height >= 0;
88
89
    try {
90
      final var protocol = getProtocol( uri );
91
      final ImageResource imageResource;
92
93
      if( protocol.isFile() ) {
94
        if( exists( Paths.get( new URI( uri ) ) ) ) {
95
          imageResource = super.get( uri, width, height );
96
        }
97
        else {
98
          throw new MissingFileException( uri );
99
        }
100
      }
101
      else if( protocol.isHttp() ) {
102
        // FlyingSaucer will silently swallow any images that fail to load.
103
        // Consequently, the following lines load the resource over HTTP and
104
        // translate errors into a broken image icon.
105
        final var url = new URL( uri );
106
        final var image = ImageIO.read( url );
107
        imageResource = new ImageResource( uri, createImage( image ) );
108
      }
109
      else {
110
        // Caught below to return a broken image; exception is swallowed.
111
        throw new UnsupportedOperationException( valueOf( protocol ) );
112
      }
113
114
      return scale( imageResource );
115
    } catch( final Exception e ) {
116
      alert( e );
117
      return new ImageResource( uri, getBrokenImage() );
118
    }
119
  }
120
121
  /**
122
   * Scales the image found at the given URI.
123
   *
124
   * @param ir {@link ImageResource} of image loaded successfully.
125
   * @return Resource representing the rendered image and path.
126
   */
127
  private ImageResource scale( final ImageResource ir ) {
128
    final var image = ir.getImage();
129
    final var imageWidth = image.getWidth();
130
    final var imageHeight = image.getHeight();
131
132
    int maxWidth = mWidthProperty.get();
133
    int newWidth = imageWidth;
134
    int newHeight = imageHeight;
135
136
    // Maintain aspect ratio while shrinking image to view port bounds.
137
    if( imageWidth > maxWidth ) {
138
      newWidth = maxWidth;
139
      newHeight = (newWidth * imageHeight) / imageWidth;
140
    }
141
142
    image.scale( newWidth, newHeight );
143
    return ir;
144
  }
145
146
  /**
147
   * Lazily initializes the broken image placeholder.
148
   *
149
   * @return The {@link FSImage} that represents a broken image icon.
150
   */
151
  private FSImage getBrokenImage() {
152
    final var image = mBrokenImage;
153
154
    if( image == null ) {
155
      mBrokenImage = createImage( BROKEN_IMAGE_PLACEHOLDER );
156
    }
157
158
    return mBrokenImage;
159
  }
160
}
1161
A src/main/java/com/keenwrite/preview/HTMLPreviewPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preview;
29
30
import com.keenwrite.adapters.DocumentAdapter;
31
import javafx.beans.property.BooleanProperty;
32
import javafx.beans.property.SimpleBooleanProperty;
33
import javafx.beans.value.ChangeListener;
34
import javafx.beans.value.ObservableValue;
35
import javafx.embed.swing.SwingNode;
36
import javafx.scene.Node;
37
import org.jsoup.Jsoup;
38
import org.jsoup.helper.W3CDom;
39
import org.jsoup.nodes.Document;
40
import org.xhtmlrenderer.layout.SharedContext;
41
import org.xhtmlrenderer.render.Box;
42
import org.xhtmlrenderer.simple.XHTMLPanel;
43
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
44
import org.xhtmlrenderer.swing.*;
45
46
import javax.swing.*;
47
import java.awt.*;
48
import java.awt.event.ComponentAdapter;
49
import java.awt.event.ComponentEvent;
50
import java.net.URI;
51
import java.nio.file.Path;
52
53
import static com.keenwrite.Constants.*;
54
import static com.keenwrite.StatusBarNotifier.alert;
55
import static com.keenwrite.util.ProtocolResolver.getProtocol;
56
import static java.awt.Desktop.Action.BROWSE;
57
import static java.awt.Desktop.getDesktop;
58
import static java.lang.Math.max;
59
import static javax.swing.SwingUtilities.invokeLater;
60
import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
61
62
/**
63
 * HTML preview pane is responsible for rendering an HTML document.
64
 */
65
public final class HTMLPreviewPane extends SwingNode {
66
67
  /**
68
   * Suppresses scrolling to the top on every key press.
69
   */
70
  private static class HTMLPanel extends XHTMLPanel {
71
    @Override
72
    public void resetScrollPosition() {
73
    }
74
  }
75
76
  /**
77
   * Suppresses scroll attempts until after the document has loaded.
78
   */
79
  private static final class DocumentEventHandler extends DocumentAdapter {
80
    private final BooleanProperty mReadyProperty = new SimpleBooleanProperty();
81
82
    public BooleanProperty readyProperty() {
83
      return mReadyProperty;
84
    }
85
86
    @Override
87
    public void documentStarted() {
88
      mReadyProperty.setValue( Boolean.FALSE );
89
    }
90
91
    @Override
92
    public void documentLoaded() {
93
      mReadyProperty.setValue( Boolean.TRUE );
94
    }
95
  }
96
97
  /**
98
   * Ensure that images are constrained to the panel width upon resizing.
99
   */
100
  private final class ResizeListener extends ComponentAdapter {
101
    @Override
102
    public void componentResized( final ComponentEvent e ) {
103
      setWidth( e );
104
    }
105
106
    @Override
107
    public void componentShown( final ComponentEvent e ) {
108
      setWidth( e );
109
    }
110
111
    /**
112
     * Sets the width of the {@link HTMLPreviewPane} so that images can be
113
     * scaled to fit. The scale factor is adjusted a bit below the full width
114
     * to prevent the horizontal scrollbar from appearing.
115
     *
116
     * @param event The component that defines the image scaling width.
117
     */
118
    private void setWidth( final ComponentEvent event ) {
119
      final int width = (int) (event.getComponent().getWidth() * .95);
120
      HTMLPreviewPane.this.mImageLoader.widthProperty().set( width );
121
    }
122
  }
123
124
  /**
125
   * Responsible for opening hyperlinks. External hyperlinks are opened in
126
   * the system's default browser; local file system links are opened in the
127
   * editor.
128
   */
129
  private static class HyperlinkListener extends LinkListener {
130
    @Override
131
    public void linkClicked( final BasicPanel panel, final String link ) {
132
      try {
133
        final var protocol = getProtocol( link );
134
135
        switch( protocol ) {
136
          case HTTP:
137
            final var desktop = getDesktop();
138
139
            if( desktop.isSupported( BROWSE ) ) {
140
              desktop.browse( new URI( link ) );
141
            }
142
            break;
143
          case FILE:
144
            // TODO: #88 -- publish a message to the event bus.
145
            break;
146
        }
147
      } catch( final Exception ex ) {
148
        alert( ex );
149
      }
150
    }
151
  }
152
153
  /**
154
   * The CSS must be rendered in points (pt) not pixels (px) to avoid blurry
155
   * rendering on some platforms.
156
   */
157
  private static final String HTML_PREFIX = "<!DOCTYPE html>"
158
      + "<html>"
159
      + "<head>"
160
      + "<link rel='stylesheet' href='" +
161
      HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) + "'/>"
162
      + "</head>"
163
      + "<body>";
164
165
  private static final W3CDom W3C_DOM = new W3CDom();
166
  private static final XhtmlNamespaceHandler NS_HANDLER =
167
      new XhtmlNamespaceHandler();
168
169
  private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
170
  private final int mHtmlPrefixLength;
171
172
  private final HTMLPanel mHtmlRenderer = new HTMLPanel();
173
  private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
174
  private final DocumentEventHandler mDocHandler = new DocumentEventHandler();
175
  private final CustomImageLoader mImageLoader = new CustomImageLoader();
176
177
  private Path mPath = DEFAULT_DIRECTORY;
178
179
  /**
180
   * Creates a new preview pane that can scroll to the caret position within the
181
   * document.
182
   */
183
  public HTMLPreviewPane() {
184
    setStyle( "-fx-background-color: white;" );
185
186
    // No need to append same prefix each time the HTML content is updated.
187
    mHtmlDocument.append( HTML_PREFIX );
188
    mHtmlPrefixLength = mHtmlDocument.length();
189
190
    // Inject an SVG renderer that produces high-quality SVG buffered images.
191
    final var factory = new ChainedReplacedElementFactory();
192
    factory.addFactory( new SvgReplacedElementFactory() );
193
    factory.addFactory( new SwingReplacedElementFactory(
194
        NO_OP_REPAINT_LISTENER, mImageLoader ) );
195
196
    final var context = getSharedContext();
197
    final var textRenderer = context.getTextRenderer();
198
    context.setReplacedElementFactory( factory );
199
    textRenderer.setSmoothingThreshold( 0 );
200
201
    setContent( mScrollPane );
202
    mHtmlRenderer.addDocumentListener( mDocHandler );
203
    mHtmlRenderer.addComponentListener( new ResizeListener() );
204
205
    // The default mouse click listener attempts navigation within the
206
    // preview panel. We want to usurp that behaviour to open the link in
207
    // a platform-specific browser.
208
    for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) {
209
      if( !(listener instanceof HoverListener) ) {
210
        mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener );
211
      }
212
    }
213
214
    mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() );
215
  }
216
217
  /**
218
   * Updates the internal HTML source, loads it into the preview pane, then
219
   * scrolls to the caret position.
220
   *
221
   * @param html The new HTML document to display.
222
   */
223
  public void process( final String html ) {
224
    final Document jsoupDoc = Jsoup.parse( decorate( html ) );
225
    final org.w3c.dom.Document w3cDoc = W3C_DOM.fromJsoup( jsoupDoc );
226
227
228
    // Access to a Swing component must occur from the Event Dispatch
229
    // thread according to Swing threading restrictions.
230
    invokeLater(
231
        () -> mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), NS_HANDLER )
232
    );
233
  }
234
235
  public void clear() {
236
    process( "" );
237
  }
238
239
  /**
240
   * Scrolls to an anchor link. The anchor links are injected when the
241
   * HTML document is created.
242
   *
243
   * @param id The unique anchor link identifier.
244
   */
245
  public void tryScrollTo( final int id ) {
246
    final ChangeListener<Boolean> listener = new ChangeListener<>() {
247
      @Override
248
      public void changed(
249
          final ObservableValue<? extends Boolean> observable,
250
          final Boolean oldValue,
251
          final Boolean newValue ) {
252
        if( newValue ) {
253
          scrollTo( id );
254
255
          mDocHandler.readyProperty().removeListener( this );
256
        }
257
      }
258
    };
259
260
    mDocHandler.readyProperty().addListener( listener );
261
  }
262
263
  /**
264
   * Scrolls to the closest element matching the given identifier without
265
   * waiting for the document to be ready. Be sure the document is ready
266
   * before calling this method.
267
   *
268
   * @param id Paragraph index.
269
   */
270
  public void scrollTo( final int id ) {
271
    if( id < 2 ) {
272
      scrollToTop();
273
    }
274
    else {
275
      Box box = findPrevBox( id );
276
      box = box == null ? findNextBox( id + 1 ) : box;
277
278
      if( box == null ) {
279
        scrollToBottom();
280
      }
281
      else {
282
        scrollTo( box );
283
      }
284
    }
285
  }
286
287
  private Box findPrevBox( final int id ) {
288
    int prevId = id;
289
    Box box = null;
290
291
    while( prevId > 0 && (box = getBoxById( PARAGRAPH_ID_PREFIX + prevId )) == null ) {
292
      prevId--;
293
    }
294
295
    return box;
296
  }
297
298
  private Box findNextBox( final int id ) {
299
    int nextId = id;
300
    Box box = null;
301
302
    while( nextId - id < 5 &&
303
        (box = getBoxById( PARAGRAPH_ID_PREFIX + nextId )) == null ) {
304
      nextId++;
305
    }
306
307
    return box;
308
  }
309
310
  private void scrollTo( final Point point ) {
311
    invokeLater( () -> mHtmlRenderer.scrollTo( point ) );
312
  }
313
314
  private void scrollTo( final Box box ) {
315
    scrollTo( createPoint( box ) );
316
  }
317
318
  private void scrollToY( final int y ) {
319
    scrollTo( new Point( 0, y ) );
320
  }
321
322
  private void scrollToTop() {
323
    scrollToY( 0 );
324
  }
325
326
  private void scrollToBottom() {
327
    scrollToY( mHtmlRenderer.getHeight() );
328
  }
329
330
  private Box getBoxById( final String id ) {
331
    return getSharedContext().getBoxById( id );
332
  }
333
334
  private String decorate( final String html ) {
335
    // Trim the HTML back to only the prefix.
336
    mHtmlDocument.setLength( mHtmlPrefixLength );
337
338
    // Write the HTML body element followed by closing tags.
339
    return mHtmlDocument.append( html ).toString();
340
  }
341
342
  public Path getPath() {
343
    return mPath;
344
  }
345
346
  public void setPath( final Path path ) {
347
    assert path != null;
348
    mPath = path;
349
  }
350
351
  /**
352
   * Content to embed in a panel.
353
   *
354
   * @return The content to display to the user.
355
   */
356
  public Node getNode() {
357
    return this;
358
  }
359
360
  public JScrollPane getScrollPane() {
361
    return mScrollPane;
362
  }
363
364
  public JScrollBar getVerticalScrollBar() {
365
    return getScrollPane().getVerticalScrollBar();
366
  }
367
368
  /**
369
   * Creates a {@link Point} to use as a reference for scrolling to the area
370
   * described by the given {@link Box}. The {@link Box} coordinates are used
371
   * to populate the {@link Point}'s location, with minor adjustments for
372
   * vertical centering.
373
   *
374
   * @param box The {@link Box} that represents a scrolling anchor reference.
375
   * @return A coordinate suitable for scrolling to.
376
   */
377
  private Point createPoint( final Box box ) {
378
    assert box != null;
379
380
    int x = box.getAbsX();
381
382
    // Scroll back up by half the height of the scroll bar to keep the typing
383
    // area within the view port. Otherwise the view port will have jumped too
384
    // high up and the whatever gets typed won't be visible.
385
    int y = max(
386
        box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2),
387
        0 );
388
389
    if( !box.getStyle().isInline() ) {
390
      final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() );
391
      x += margin.left();
392
      y += margin.top();
393
    }
394
395
    return new Point( x, y );
396
  }
397
398
  private String getBaseUrl() {
399
    final Path basePath = getPath();
400
    final Path parent = basePath == null ? null : basePath.getParent();
401
402
    return parent == null ? "" : parent.toUri().toString();
403
  }
404
405
  private SharedContext getSharedContext() {
406
    return mHtmlRenderer.getSharedContext();
407
  }
408
}
1409
A src/main/java/com/keenwrite/preview/MathRenderer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preview;
29
30
import com.whitemagicsoftware.tex.*;
31
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
32
import org.w3c.dom.Document;
33
34
import java.util.function.Supplier;
35
36
import static com.keenwrite.StatusBarNotifier.alert;
37
38
/**
39
 * Responsible for rendering formulas as scalable vector graphics (SVG).
40
 */
41
public class MathRenderer {
42
43
  /**
44
   * Default font size in points.
45
   */
46
  private static final float FONT_SIZE = 20f;
47
48
  private final TeXFont mTeXFont = createDefaultTeXFont( FONT_SIZE );
49
  private final TeXEnvironment mEnvironment = createTeXEnvironment( mTeXFont );
50
  private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D();
51
52
  public MathRenderer() {
53
    mGraphics.scale( FONT_SIZE, FONT_SIZE );
54
  }
55
56
  /**
57
   * This method only takes a few seconds to generate
58
   *
59
   * @param equation A mathematical expression to render.
60
   * @return The given string with all formulas transformed into SVG format.
61
   */
62
  public Document render( final String equation ) {
63
    final var formula = new TeXFormula( equation );
64
    final var box = formula.createBox( mEnvironment );
65
    final var l = new TeXLayout( box, FONT_SIZE );
66
67
    mGraphics.initialize( l.getWidth(), l.getHeight() );
68
    box.draw( mGraphics, l.getX(), l.getY() );
69
    return mGraphics.toDom();
70
  }
71
72
  @SuppressWarnings("SameParameterValue")
73
  private TeXFont createDefaultTeXFont( final float fontSize ) {
74
    return create( () -> new DefaultTeXFont( fontSize ) );
75
  }
76
77
  private TeXEnvironment createTeXEnvironment( final TeXFont texFont ) {
78
    return create( () -> new TeXEnvironment( texFont ) );
79
  }
80
81
  private SvgDomGraphics2D createSvgDomGraphics2D() {
82
    return create( SvgDomGraphics2D::new );
83
  }
84
85
  /**
86
   * Tries to instantiate a given object, returning {@code null} on failure.
87
   * The failure message is bubbled up to to the user interface.
88
   *
89
   * @param supplier Creates an instance.
90
   * @param <T>      The type of instance being created.
91
   * @return An instance of the parameterized type or {@code null} upon error.
92
   */
93
  private <T> T create( final Supplier<T> supplier ) {
94
    try {
95
      return supplier.get();
96
    } catch( final Exception ex ) {
97
      alert( ex );
98
      return null;
99
    }
100
  }
101
}
1102
A src/main/java/com/keenwrite/preview/RenderingSettings.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preview;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
33
import static java.awt.RenderingHints.*;
34
import static java.awt.Toolkit.getDefaultToolkit;
35
36
/**
37
 * Responsible for supplying consistent rendering hints throughout the
38
 * application, such as image rendering for {@link SvgRasterizer}.
39
 */
40
@SuppressWarnings("rawtypes")
41
public class RenderingSettings {
42
43
  /**
44
   * Default hints for high-quality rendering that may be changed by
45
   * the system's rendering hints.
46
   */
47
  private static final Map<Object, Object> DEFAULT_HINTS = Map.of(
48
      KEY_ANTIALIASING,
49
      VALUE_ANTIALIAS_ON,
50
      KEY_ALPHA_INTERPOLATION,
51
      VALUE_ALPHA_INTERPOLATION_QUALITY,
52
      KEY_COLOR_RENDERING,
53
      VALUE_COLOR_RENDER_QUALITY,
54
      KEY_DITHERING,
55
      VALUE_DITHER_DISABLE,
56
      KEY_FRACTIONALMETRICS,
57
      VALUE_FRACTIONALMETRICS_ON,
58
      KEY_INTERPOLATION,
59
      VALUE_INTERPOLATION_BICUBIC,
60
      KEY_RENDERING,
61
      VALUE_RENDER_QUALITY,
62
      KEY_STROKE_CONTROL,
63
      VALUE_STROKE_PURE,
64
      KEY_TEXT_ANTIALIASING,
65
      VALUE_TEXT_ANTIALIAS_ON
66
  );
67
68
  /**
69
   * Shared hints for high-quality rendering.
70
   */
71
  public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>(
72
      DEFAULT_HINTS
73
  );
74
75
  static {
76
    final var toolkit = getDefaultToolkit();
77
    final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" );
78
79
    if( hints instanceof Map ) {
80
      final var map = (Map) hints;
81
      for( final var key : map.keySet() ) {
82
        final var hint = map.get( key );
83
        RENDERING_HINTS.put( key, hint );
84
      }
85
    }
86
  }
87
88
  /**
89
   * Prevent instantiation as per Joshua Bloch's recommendation.
90
   */
91
  private RenderingSettings() {
92
  }
93
}
194
A src/main/java/com/keenwrite/preview/SvgRasterizer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preview;
29
30
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
31
import org.apache.batik.gvt.renderer.ImageRenderer;
32
import org.apache.batik.transcoder.TranscoderException;
33
import org.apache.batik.transcoder.TranscoderInput;
34
import org.apache.batik.transcoder.TranscoderOutput;
35
import org.apache.batik.transcoder.image.ImageTranscoder;
36
import org.w3c.dom.Document;
37
import org.w3c.dom.Element;
38
39
import javax.xml.transform.Transformer;
40
import javax.xml.transform.TransformerConfigurationException;
41
import javax.xml.transform.TransformerFactory;
42
import javax.xml.transform.dom.DOMSource;
43
import javax.xml.transform.stream.StreamResult;
44
import java.awt.*;
45
import java.awt.image.BufferedImage;
46
import java.io.IOException;
47
import java.io.StringReader;
48
import java.io.StringWriter;
49
import java.net.URL;
50
import java.text.NumberFormat;
51
52
import static com.keenwrite.StatusBarNotifier.alert;
53
import static com.keenwrite.preview.RenderingSettings.RENDERING_HINTS;
54
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
55
import static java.nio.charset.StandardCharsets.UTF_8;
56
import static java.text.NumberFormat.getIntegerInstance;
57
import static javax.xml.transform.OutputKeys.*;
58
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
59
import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
60
61
/**
62
 * Responsible for converting SVG images into rasterized PNG images.
63
 */
64
public class SvgRasterizer {
65
  private static final SAXSVGDocumentFactory FACTORY_DOM =
66
      new SAXSVGDocumentFactory( getXMLParserClassName() );
67
68
  private static final TransformerFactory FACTORY_TRANSFORM =
69
      TransformerFactory.newInstance();
70
71
  private static final Transformer sTransformer;
72
73
  static {
74
    Transformer t;
75
76
    try {
77
      t = FACTORY_TRANSFORM.newTransformer();
78
      t.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
79
      t.setOutputProperty( METHOD, "xml" );
80
      t.setOutputProperty( INDENT, "no" );
81
      t.setOutputProperty( ENCODING, UTF_8.name() );
82
    } catch( final TransformerConfigurationException e ) {
83
      t = null;
84
    }
85
86
    sTransformer = t;
87
  }
88
89
  private static final NumberFormat INT_FORMAT = getIntegerInstance();
90
91
  public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER;
92
93
  /**
94
   * A FontAwesome camera icon, cleft asunder.
95
   */
96
  public static final String BROKEN_IMAGE_SVG =
97
      "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" +
98
          ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" +
99
          ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " +
100
          "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" +
101
          ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" +
102
          ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" +
103
          ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" +
104
          ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " +
105
          "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" +
106
          ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" +
107
          ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" +
108
          ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" +
109
          ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" +
110
          ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" +
111
          ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" +
112
          ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" +
113
          ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " +
114
          ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" +
115
          ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" +
116
          ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" +
117
          ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" +
118
          ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" +
119
          ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" +
120
          ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " +
121
          "0'/></g></svg>";
122
123
  static {
124
    // The width and height cannot be embedded in the SVG above because the
125
    // path element values are relative to the viewBox dimensions.
126
    final int w = 75;
127
    final int h = 75;
128
    BufferedImage image;
129
130
    try {
131
      image = rasterizeString( BROKEN_IMAGE_SVG, w );
132
    } catch( final Exception e ) {
133
      image = new BufferedImage( w, h, TYPE_INT_RGB );
134
      final var graphics = (Graphics2D) image.getGraphics();
135
      graphics.setRenderingHints( RENDERING_HINTS );
136
137
      // Fall back to a (\) symbol.
138
      graphics.setColor( new Color( 204, 204, 204 ) );
139
      graphics.fillRect( 0, 0, w, h );
140
      graphics.setColor( new Color( 255, 204, 204 ) );
141
      graphics.setStroke( new BasicStroke( 4 ) );
142
      graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
143
      graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
144
                         h / 4 + (int) (w / 4 / Math.PI),
145
                         w / 2 + w / 4 - (int) (w / 4 / Math.PI),
146
                         h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
147
    }
148
149
    BROKEN_IMAGE_PLACEHOLDER = image;
150
  }
151
152
  /**
153
   * Responsible for creating a new {@link ImageRenderer} implementation that
154
   * can render a DOM as an SVG image.
155
   */
156
  private static class BufferedImageTranscoder extends ImageTranscoder {
157
    private BufferedImage mImage;
158
159
    @Override
160
    public BufferedImage createImage( final int w, final int h ) {
161
      return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
162
    }
163
164
    @Override
165
    public void writeImage(
166
        final BufferedImage image, final TranscoderOutput output ) {
167
      mImage = image;
168
    }
169
170
    public BufferedImage getImage() {
171
      return mImage;
172
    }
173
174
    @Override
175
    protected ImageRenderer createRenderer() {
176
      final ImageRenderer renderer = super.createRenderer();
177
      final RenderingHints hints = renderer.getRenderingHints();
178
      hints.putAll( RENDERING_HINTS );
179
180
      renderer.setRenderingHints( hints );
181
182
      return renderer;
183
    }
184
  }
185
186
  /**
187
   * Rasterizes the vector graphic file at the given URL. If any exception
188
   * happens, a red circle is returned instead.
189
   *
190
   * @param url   The URL to a vector graphic file, which must include the
191
   *              protocol scheme (such as file:// or https://).
192
   * @param width The number of pixels wide to render the image. The aspect
193
   *              ratio is maintained.
194
   * @return Either the rasterized image upon success or a red circle.
195
   */
196
  public static BufferedImage rasterize( final String url, final int width ) {
197
    try {
198
      return rasterize( new URL( url ), width );
199
    } catch( final Exception ex ) {
200
      alert( ex );
201
      return BROKEN_IMAGE_PLACEHOLDER;
202
    }
203
  }
204
205
  /**
206
   * Rasterizes the given document into an image.
207
   *
208
   * @param svg   The SVG {@link Document} to rasterize.
209
   * @param width The rasterized image's width (in pixels).
210
   * @return The rasterized image.
211
   * @throws TranscoderException Signifies an issue with the input document.
212
   */
213
  public static BufferedImage rasterize( final Document svg, final int width )
214
      throws TranscoderException {
215
    final var transcoder = new BufferedImageTranscoder();
216
    final var input = new TranscoderInput( svg );
217
218
    transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
219
    transcoder.transcode( input, null );
220
221
    return transcoder.getImage();
222
  }
223
224
  /**
225
   * Converts an SVG drawing into a rasterized image that can be drawn on
226
   * a graphics context.
227
   *
228
   * @param url   The path to the image (can be web address).
229
   * @param width Scale the image width to this size (aspect ratio is
230
   *              maintained).
231
   * @return The vector graphic transcoded into a raster image format.
232
   * @throws IOException         Could not read the vector graphic.
233
   * @throws TranscoderException Could not convert the vector graphic to an
234
   *                             instance of {@link Image}.
235
   */
236
  public static BufferedImage rasterize( final URL url, final int width )
237
      throws IOException, TranscoderException {
238
    return rasterize( FACTORY_DOM.createDocument( url.toString() ), width );
239
  }
240
241
  public static BufferedImage rasterize( final Document document ) {
242
    try {
243
      final var root = document.getDocumentElement();
244
      final var width = root.getAttribute( "width" );
245
      return rasterize( document, INT_FORMAT.parse( width ).intValue() );
246
    } catch( final Exception ex ) {
247
      alert( ex );
248
      return BROKEN_IMAGE_PLACEHOLDER;
249
    }
250
  }
251
252
  /**
253
   * Converts an SVG string into a rasterized image that can be drawn on
254
   * a graphics context.
255
   *
256
   * @param svg The SVG xml document.
257
   * @param w   Scale the image width to this size (aspect ratio is
258
   *            maintained).
259
   * @return The vector graphic transcoded into a raster image format.
260
   * @throws TranscoderException Could not convert the vector graphic to an
261
   *                             instance of {@link Image}.
262
   */
263
  public static BufferedImage rasterizeString( final String svg, final int w )
264
      throws IOException, TranscoderException {
265
    return rasterize( toDocument( svg ), w );
266
  }
267
268
  /**
269
   * Converts an SVG string into a rasterized image that can be drawn on
270
   * a graphics context. The dimensions are determined from the document.
271
   *
272
   * @param xml The SVG xml document.
273
   * @return The vector graphic transcoded into a raster image format.
274
   */
275
  public static BufferedImage rasterizeString( final String xml ) {
276
    try {
277
      final var document = toDocument( xml );
278
      final var root = document.getDocumentElement();
279
      final var width = root.getAttribute( "width" );
280
      return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() );
281
    } catch( final Exception ex ) {
282
      alert( ex );
283
      return BROKEN_IMAGE_PLACEHOLDER;
284
    }
285
  }
286
287
  /**
288
   * Converts an SVG XML string into a new {@link Document} instance.
289
   *
290
   * @param xml The XML containing SVG elements.
291
   * @return The SVG contents parsed into a {@link Document} object model.
292
   * @throws IOException Could
293
   */
294
  private static Document toDocument( final String xml ) throws IOException {
295
    try( final var reader = new StringReader( xml ) ) {
296
      return FACTORY_DOM.createSVGDocument(
297
          "http://www.w3.org/2000/svg", reader );
298
    }
299
  }
300
301
  /**
302
   * Given a document object model (DOM) {@link Element}, this will convert that
303
   * element to a string.
304
   *
305
   * @param e The DOM node to convert to a string.
306
   * @return The DOM node as an escaped, plain text string.
307
   */
308
  public static String toSvg( final Element e ) {
309
    try( final var writer = new StringWriter() ) {
310
      sTransformer.transform( new DOMSource( e ), new StreamResult( writer ) );
311
      return writer.toString().replaceAll( "xmlns=\"\" ", "" );
312
    } catch( final Exception ex ) {
313
      alert( ex );
314
    }
315
316
    return BROKEN_IMAGE_SVG;
317
  }
318
}
1319
A src/main/java/com/keenwrite/preview/SvgReplacedElementFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.preview;
29
30
import com.keenwrite.util.BoundedCache;
31
import org.apache.commons.io.FilenameUtils;
32
import org.w3c.dom.Element;
33
import org.xhtmlrenderer.extend.ReplacedElement;
34
import org.xhtmlrenderer.extend.ReplacedElementFactory;
35
import org.xhtmlrenderer.extend.UserAgentCallback;
36
import org.xhtmlrenderer.layout.LayoutContext;
37
import org.xhtmlrenderer.render.BlockBox;
38
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
39
import org.xhtmlrenderer.swing.ImageReplacedElement;
40
41
import java.awt.image.BufferedImage;
42
import java.util.Map;
43
import java.util.function.Function;
44
45
import static com.keenwrite.StatusBarNotifier.alert;
46
import static com.keenwrite.preview.SvgRasterizer.rasterize;
47
import static com.keenwrite.processors.markdown.tex.TeXNode.HTML_TEX;
48
49
/**
50
 * Responsible for running {@link SvgRasterizer} on SVG images detected within
51
 * a document to transform them into rasterized versions.
52
 */
53
public class SvgReplacedElementFactory implements ReplacedElementFactory {
54
55
  /**
56
   * Prevent instantiation until needed.
57
   */
58
  private static class MathRendererContainer {
59
    private static final MathRenderer INSTANCE = new MathRenderer();
60
  }
61
62
  /**
63
   * Returns the singleton instance for rendering math symbols.
64
   *
65
   * @return A non-null instance, loaded, configured, and ready to render math.
66
   */
67
  public static MathRenderer getInstance() {
68
    return MathRendererContainer.INSTANCE;
69
  }
70
71
  /**
72
   * SVG filename extension maps to an SVG image element.
73
   */
74
  private static final String SVG_FILE = "svg";
75
76
  private static final String HTML_IMAGE = "img";
77
  private static final String HTML_IMAGE_SRC = "src";
78
79
  /**
80
   * A bounded cache that removes the oldest image if the maximum number of
81
   * cached images has been reached. This constrains the number of images
82
   * loaded into memory.
83
   */
84
  private final Map<String, BufferedImage> mImageCache =
85
      new BoundedCache<>( 150 );
86
87
  @Override
88
  public ReplacedElement createReplacedElement(
89
      final LayoutContext c,
90
      final BlockBox box,
91
      final UserAgentCallback uac,
92
      final int cssWidth,
93
      final int cssHeight ) {
94
    BufferedImage image = null;
95
    final var e = box.getElement();
96
97
    if( e != null ) {
98
      try {
99
        final var nodeName = e.getNodeName();
100
101
        if( HTML_IMAGE.equals( nodeName ) ) {
102
          final var src = e.getAttribute( HTML_IMAGE_SRC );
103
          final var ext = FilenameUtils.getExtension( src );
104
105
          if( SVG_FILE.equalsIgnoreCase( ext ) ) {
106
            image = getCachedImage(
107
                src, svg -> rasterize( svg, box.getContentWidth() ) );
108
          }
109
        }
110
        else if( HTML_TEX.equals( nodeName ) ) {
111
          // Convert the TeX element to a raster graphic if not yet cached.
112
          final var src = e.getTextContent();
113
          image = getCachedImage(
114
              src, __ -> rasterize( getInstance().render( src ) )
115
          );
116
        }
117
      } catch( final Exception ex ) {
118
        alert( ex );
119
      }
120
    }
121
122
    if( image != null ) {
123
      final var w = image.getWidth( null );
124
      final var h = image.getHeight( null );
125
126
      return new ImageReplacedElement( image, w, h );
127
    }
128
129
    return null;
130
  }
131
132
  @Override
133
  public void reset() {
134
  }
135
136
  @Override
137
  public void remove( final Element e ) {
138
  }
139
140
  @Override
141
  public void setFormSubmissionListener( FormSubmissionListener listener ) {
142
  }
143
144
  /**
145
   * Returns an image associated with a string; the string's pre-computed
146
   * hash code is returned as the string value, making this operation very
147
   * quick to return the corresponding {@link BufferedImage}.
148
   *
149
   * @param src        The source used for the key into the image cache.
150
   * @param rasterizer {@link Function} to call to rasterize an image.
151
   * @return The image that corresponds to the given source string.
152
   */
153
  private BufferedImage getCachedImage(
154
      final String src, final Function<String, BufferedImage> rasterizer ) {
155
    return mImageCache.computeIfAbsent( src, __ -> rasterizer.apply( src ) );
156
  }
157
}
1158
A src/main/java/com/keenwrite/processors/AbstractProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
/**
31
 * Responsible for transforming a document through a variety of chained
32
 * handlers. If there are conditions where this handler should not process the
33
 * entire chain, create a second handler, or split the chain into reusable
34
 * sub-chains.
35
 *
36
 * @param <T> The type of object to process.
37
 */
38
public abstract class AbstractProcessor<T> implements Processor<T> {
39
40
  /**
41
   * Used while processing the entire chain; null to signify no more links.
42
   */
43
  private final Processor<T> mNext;
44
45
  /**
46
   * Constructs a new default handler with no successor.
47
   */
48
  protected AbstractProcessor() {
49
    this( null );
50
  }
51
52
  /**
53
   * Constructs a new default handler with a given successor.
54
   *
55
   * @param successor The next processor in the chain.
56
   */
57
  public AbstractProcessor( final Processor<T> successor ) {
58
    mNext = successor;
59
  }
60
61
  @Override
62
  public Processor<T> next() {
63
    return mNext;
64
  }
65
66
  /**
67
   * This algorithm is incorrect, but works for the one use case of removing
68
   * the ending HTML Preview Processor from the end of the processor chain.
69
   * The processor chain is immutable so this creates a succession of
70
   * delegators that wrap each processor in the chain, except for the one
71
   * to be removed.
72
   * <p>
73
   * An alternative is to update the {@link ProcessorFactory} with the ability
74
   * to create a processor chain devoid of an {@link HtmlPreviewProcessor}.
75
   * </p>
76
   *
77
   * @param removal The {@link Processor} to remove from the chain.
78
   * @return A delegating processor chain starting from this processor
79
   * onwards with the given processor removed from the chain.
80
   */
81
  @Override
82
  public Processor<T> remove( final Class<? extends Processor<T>> removal ) {
83
    Processor<T> p = this;
84
    final ProcessorDelegator<T> head = new ProcessorDelegator<>( p );
85
    ProcessorDelegator<T> result = head;
86
87
    while( p != null ) {
88
      final Processor<T> next = p.next();
89
90
      if( next != null && next.getClass() != removal ) {
91
        final var delegator = new ProcessorDelegator<>( next );
92
93
        result.setNext( delegator );
94
        result = delegator;
95
      }
96
97
      p = p.next();
98
    }
99
100
    return head;
101
  }
102
103
  private static final class ProcessorDelegator<T>
104
      extends AbstractProcessor<T> {
105
    private final Processor<T> mDelegate;
106
    private Processor<T> mNext;
107
108
    public ProcessorDelegator( final Processor<T> delegate ) {
109
      super( delegate );
110
111
      assert delegate != null;
112
113
      mDelegate = delegate;
114
    }
115
116
    @Override
117
    public T apply( T t ) {
118
      return mDelegate.apply( t );
119
    }
120
121
    protected void setNext( final Processor<T> next ) {
122
      mNext = next;
123
    }
124
125
    @Override
126
    public Processor<T> next() {
127
      return mNext;
128
    }
129
  }
130
}
1131
A src/main/java/com/keenwrite/processors/DefinitionProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
import java.util.Map;
31
32
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
33
34
/**
35
 * Processes interpolated string definitions in the document and inserts
36
 * their values into the post-processed text. The default variable syntax is
37
 * {@code $variable$}.
38
 */
39
public class DefinitionProcessor extends AbstractProcessor<String> {
40
41
  private final Map<String, String> mDefinitions;
42
43
  public DefinitionProcessor(
44
      final Processor<String> successor, final Map<String, String> map ) {
45
    super( successor );
46
    mDefinitions = map;
47
  }
48
49
  /**
50
   * Processes the given text document by replacing variables with their values.
51
   *
52
   * @param text The document text that includes variables that should be
53
   *             replaced with values when rendered as HTML.
54
   * @return The text with all variables replaced.
55
   */
56
  @Override
57
  public String apply( final String text ) {
58
    return replace( text, getDefinitions() );
59
  }
60
61
  /**
62
   * Returns the map to use for variable substitution.
63
   *
64
   * @return A map of variable names to values.
65
   */
66
  protected Map<String, String> getDefinitions() {
67
    return mDefinitions;
68
  }
69
}
170
A src/main/java/com/keenwrite/processors/HtmlPreviewProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
import com.keenwrite.preview.HTMLPreviewPane;
31
32
/**
33
 * Responsible for notifying the HTMLPreviewPane when the succession chain has
34
 * updated. This decouples knowledge of changes to the editor panel from the
35
 * HTML preview panel as well as any processing that takes place before the
36
 * final HTML preview is rendered. This should be the last link in the processor
37
 * chain.
38
 */
39
public class HtmlPreviewProcessor extends AbstractProcessor<String> {
40
41
  // There is only one preview panel.
42
  private static HTMLPreviewPane sHtmlPreviewPane;
43
44
  /**
45
   * Constructs the end of a processing chain.
46
   *
47
   * @param htmlPreviewPane The pane to update with the post-processed document.
48
   */
49
  public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
50
    sHtmlPreviewPane = htmlPreviewPane;
51
  }
52
53
  /**
54
   * Update the preview panel using HTML from the succession chain.
55
   *
56
   * @param html The document content to render in the preview pane. The HTML
57
   *             should not contain a doctype, head, or body tag, only
58
   *             content to render within the body.
59
   * @return {@code null} to indicate no more processors in the chain.
60
   */
61
  @Override
62
  public String apply( final String html ) {
63
    getHtmlPreviewPane().process( html );
64
65
    // No more processing required.
66
    return null;
67
  }
68
69
  private HTMLPreviewPane getHtmlPreviewPane() {
70
    return sHtmlPreviewPane;
71
  }
72
}
173
A src/main/java/com/keenwrite/processors/IdentityProcessor.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
/**
31
 * This is the default processor used when an unknown filename extension is
32
 * encountered.
33
 */
34
public class IdentityProcessor extends AbstractProcessor<String> {
35
36
  /**
37
   * Passes the link to the super constructor.
38
   *
39
   * @param successor The next processor in the chain to use for text
40
   *                  processing.
41
   */
42
  public IdentityProcessor( final Processor<String> successor ) {
43
    super( successor );
44
  }
45
46
  /**
47
   * Returns the given string, modified with "pre" tags.
48
   *
49
   * @param t The string to return, enclosed in "pre" tags.
50
   * @return The value of t wrapped in "pre" tags.
51
   */
52
  @Override
53
  public String apply( final String t ) {
54
    return "<pre>" + t + "</pre>";
55
  }
56
}
157
A src/main/java/com/keenwrite/processors/InlineRProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
import com.keenwrite.preferences.UserPreferences;
31
import javafx.beans.property.ObjectProperty;
32
import javafx.beans.property.StringProperty;
33
34
import javax.script.ScriptEngine;
35
import javax.script.ScriptEngineManager;
36
import java.io.File;
37
import java.nio.file.Path;
38
import java.util.LinkedHashMap;
39
import java.util.Map;
40
import java.util.concurrent.atomic.AtomicBoolean;
41
42
import static com.keenwrite.Constants.STATUS_PARSE_ERROR;
43
import static com.keenwrite.StatusBarNotifier.alert;
44
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
45
import static com.keenwrite.sigils.RSigilOperator.PREFIX;
46
import static com.keenwrite.sigils.RSigilOperator.SUFFIX;
47
import static java.lang.Math.min;
48
49
/**
50
 * Transforms a document containing R statements into Markdown.
51
 */
52
public final class InlineRProcessor extends DefinitionProcessor {
53
  /**
54
   * Constrain memory when typing new R expressions into the document.
55
   */
56
  private static final int MAX_CACHED_R_STATEMENTS = 512;
57
58
  /**
59
   * Where to put document inline evaluated R expressions.
60
   */
61
  private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
62
    @Override
63
    protected boolean removeEldestEntry(
64
        final Map.Entry<String, Object> eldest ) {
65
      return size() > MAX_CACHED_R_STATEMENTS;
66
    }
67
  };
68
69
  /**
70
   * Only one editor is open at a time.
71
   */
72
  private static final ScriptEngine ENGINE =
73
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
74
75
  private static final int PREFIX_LENGTH = PREFIX.length();
76
77
  private final AtomicBoolean mDirty = new AtomicBoolean( false );
78
79
  /**
80
   * Constructs a processor capable of evaluating R statements.
81
   *
82
   * @param successor Subsequent link in the processing chain.
83
   * @param map       Resolved definitions map.
84
   */
85
  public InlineRProcessor(
86
      final Processor<String> successor,
87
      final Map<String, String> map ) {
88
    super( successor, map );
89
90
    bootstrapScriptProperty().addListener(
91
        ( ob, oldScript, newScript ) -> setDirty( true ) );
92
    workingDirectoryProperty().addListener(
93
        ( ob, oldScript, newScript ) -> setDirty( true ) );
94
95
    getUserPreferences().addSaveEventHandler( ( handler ) -> {
96
      if( isDirty() ) {
97
        init();
98
        setDirty( false );
99
      }
100
    } );
101
102
    init();
103
  }
104
105
  /**
106
   * Initialises the R code so that R can find imported libraries. Note that
107
   * any existing R functionality will not be overwritten if this method is
108
   * called multiple times.
109
   */
110
  private void init() {
111
    final var bootstrap = getBootstrapScript();
112
113
    if( !bootstrap.isBlank() ) {
114
      final var wd = getWorkingDirectory();
115
      final var dir = wd.toString().replace( '\\', '/' );
116
      final var map = getDefinitions();
117
      map.put( "$application.r.working.directory$", dir );
118
119
      eval( replace( bootstrap, map ) );
120
    }
121
  }
122
123
  /**
124
   * Sets the dirty flag to indicate that the bootstrap script or working
125
   * directory has been modified. Upon saving the preferences, if this flag
126
   * is true, then {@link #init()} will be called to reload the R environment.
127
   *
128
   * @param dirty Set to true to reload changes upon closing preferences.
129
   */
130
  private void setDirty( final boolean dirty ) {
131
    mDirty.set( dirty );
132
  }
133
134
  /**
135
   * Answers whether R-related settings have been modified.
136
   *
137
   * @return {@code true} when the settings have changed.
138
   */
139
  private boolean isDirty() {
140
    return mDirty.get();
141
  }
142
143
  /**
144
   * Evaluates all R statements in the source document and inserts the
145
   * calculated value into the generated document.
146
   *
147
   * @param text The document text that includes variables that should be
148
   *             replaced with values when rendered as HTML.
149
   * @return The generated document with output from all R statements
150
   * substituted with value returned from their execution.
151
   */
152
  @Override
153
  public String apply( final String text ) {
154
    final int length = text.length();
155
156
    // The * 2 is a wild guess at the ratio of R statements to the length
157
    // of text produced by those statements.
158
    final StringBuilder sb = new StringBuilder( length * 2 );
159
160
    int prevIndex = 0;
161
    int currIndex = text.indexOf( PREFIX );
162
163
    while( currIndex >= 0 ) {
164
      // Copy everything up to, but not including, an R statement (`r#).
165
      sb.append( text, prevIndex, currIndex );
166
167
      // Jump to the start of the R statement.
168
      prevIndex = currIndex + PREFIX_LENGTH;
169
170
      // Find the statement ending (`), without indexing past the text boundary.
171
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
172
173
      // Only evaluate inline R statements that have end delimiters.
174
      if( currIndex > 1 ) {
175
        // Extract the inline R statement to be evaluated.
176
        final String r = text.substring( prevIndex, currIndex );
177
178
        // Pass the R statement into the R engine for evaluation.
179
        try {
180
          final Object result = evalText( r );
181
182
          // Append the string representation of the result into the text.
183
          sb.append( result );
184
        } catch( final Exception e ) {
185
          // If the string couldn't be parsed using R, append the statement
186
          // that failed to parse, instead of its evaluated value.
187
          sb.append( PREFIX ).append( r ).append( SUFFIX );
188
189
          // Tell the user that there was a problem.
190
          alert( STATUS_PARSE_ERROR, e.getMessage(), currIndex );
191
        }
192
193
        // Retain the R statement's ending position in the text.
194
        prevIndex = currIndex + 1;
195
      }
196
197
      // Find the start of the next inline R statement.
198
      currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
199
    }
200
201
    // Copy from the previous index to the end of the string.
202
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
203
  }
204
205
  /**
206
   * Look up an R expression from the cache then return the resulting object.
207
   * If the R expression hasn't been cached, it'll first be evaluated.
208
   *
209
   * @param r The expression to evaluate.
210
   * @return The object resulting from the evaluation.
211
   */
212
  private Object evalText( final String r ) {
213
    return mEvalCache.computeIfAbsent( r, v -> eval( r ) );
214
  }
215
216
  /**
217
   * Evaluate an R expression and return the resulting object.
218
   *
219
   * @param r The expression to evaluate.
220
   * @return The object resulting from the evaluation.
221
   */
222
  private Object eval( final String r ) {
223
    try {
224
      return getScriptEngine().eval( r );
225
    } catch( final Exception ex ) {
226
      final String expr = r.substring( 0, min( r.length(), 30 ) );
227
      alert( "Main.status.error.r", expr, ex.getMessage() );
228
    }
229
230
    return "";
231
  }
232
233
  /**
234
   * Return the given path if not {@code null}, otherwise return the path to
235
   * the user's directory.
236
   *
237
   * @return A non-null path.
238
   */
239
  private Path getWorkingDirectory() {
240
    return getUserPreferences().getRDirectory().toPath();
241
  }
242
243
  private ObjectProperty<File> workingDirectoryProperty() {
244
    return getUserPreferences().rDirectoryProperty();
245
  }
246
247
  /**
248
   * Loads the R init script from the application's persisted preferences.
249
   *
250
   * @return A non-null string, possibly empty.
251
   */
252
  private String getBootstrapScript() {
253
    return getUserPreferences().getRScript();
254
  }
255
256
  private StringProperty bootstrapScriptProperty() {
257
    return getUserPreferences().rScriptProperty();
258
  }
259
260
  private UserPreferences getUserPreferences() {
261
    return UserPreferences.getInstance();
262
  }
263
264
  private ScriptEngine getScriptEngine() {
265
    return ENGINE;
266
  }
267
}
1268
A src/main/java/com/keenwrite/processors/Processor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
import java.util.function.UnaryOperator;
31
32
/**
33
 * Responsible for processing documents from one known format to another.
34
 * Processes the given content providing a transformation from one document
35
 * format into another. For example, this could convert from XML to text using
36
 * an XSLT processor, or from markdown to HTML.
37
 *
38
 * @param <T> The type of processor to create.
39
 */
40
public interface Processor<T> extends UnaryOperator<T> {
41
42
  /**
43
   * Removes the given processor from the chain, returning a new immutable
44
   * chain equivalent to this chain, but without the given processor.
45
   *
46
   * @param processor The {@link Processor} to remove from the chain.
47
   * @return A delegating processor chain starting from this processor
48
   * onwards with the given processor removed from the chain.
49
   */
50
  Processor<T> remove( Class<? extends Processor<T>> processor );
51
52
  /**
53
   * Adds a document processor to call after this processor finishes processing
54
   * the document given to the process method.
55
   *
56
   * @return The processor that should transform the document after this
57
   * instance has finished processing, or {@code null} if this is the last
58
   * processor in the chain.
59
   */
60
  default Processor<T> next() {
61
    return null;
62
  }
63
}
164
A src/main/java/com/keenwrite/processors/ProcessorFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
import com.keenwrite.AbstractFileFactory;
31
import com.keenwrite.FileEditorTab;
32
import com.keenwrite.preview.HTMLPreviewPane;
33
import com.keenwrite.processors.markdown.MarkdownProcessor;
34
35
import java.util.Map;
36
37
/**
38
 * Responsible for creating processors capable of parsing, transforming,
39
 * interpolating, and rendering known file types.
40
 */
41
public class ProcessorFactory extends AbstractFileFactory {
42
43
  private final HTMLPreviewPane mPreviewPane;
44
  private final Map<String, String> mResolvedMap;
45
  private final Processor<String> mMarkdownProcessor;
46
47
  /**
48
   * Constructs a factory with the ability to create processors that can perform
49
   * text and caret processing to generate a final preview.
50
   *
51
   * @param previewPane Where the final output is rendered.
52
   * @param resolvedMap Flat map of definitions to replace before final render.
53
   */
54
  public ProcessorFactory(
55
      final HTMLPreviewPane previewPane,
56
      final Map<String, String> resolvedMap ) {
57
    mPreviewPane = previewPane;
58
    mResolvedMap = resolvedMap;
59
    mMarkdownProcessor = createMarkdownProcessor();
60
  }
61
62
  /**
63
   * Creates a processor chain suitable for parsing and rendering the file
64
   * opened at the given tab.
65
   *
66
   * @param tab The tab containing a text editor, path, and caret position.
67
   * @return A processor that can render the given tab's text.
68
   */
69
  public Processor<String> createProcessors( final FileEditorTab tab ) {
70
    return switch( lookup( tab.getPath() ) ) {
71
      case RMARKDOWN -> createRProcessor();
72
      case SOURCE -> createMarkdownDefinitionProcessor();
73
      case XML -> createXMLProcessor( tab );
74
      case RXML -> createRXMLProcessor( tab );
75
      default -> createIdentityProcessor();
76
    };
77
  }
78
79
  private Processor<String> createHTMLPreviewProcessor() {
80
    return new HtmlPreviewProcessor( getPreviewPane() );
81
  }
82
83
  /**
84
   * Creates and links the processors at the end of the processing chain.
85
   *
86
   * @return A markdown, caret replacement, and preview pane processor chain.
87
   */
88
  private Processor<String> createMarkdownProcessor() {
89
    final var hpp = createHTMLPreviewProcessor();
90
    return new MarkdownProcessor( hpp, getPreviewPane().getPath() );
91
  }
92
93
  protected Processor<String> createIdentityProcessor() {
94
    final var hpp = createHTMLPreviewProcessor();
95
    return new IdentityProcessor( hpp );
96
  }
97
98
  protected Processor<String> createDefinitionProcessor(
99
      final Processor<String> p ) {
100
    return new DefinitionProcessor( p, getResolvedMap() );
101
  }
102
103
  protected Processor<String> createMarkdownDefinitionProcessor() {
104
    final var tpc = getCommonProcessor();
105
    return createDefinitionProcessor( tpc );
106
  }
107
108
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
109
    final var tpc = getCommonProcessor();
110
    final var xmlp = new XmlProcessor( tpc, tab.getPath() );
111
    return createDefinitionProcessor( xmlp );
112
  }
113
114
  protected Processor<String> createRProcessor() {
115
    final var tpc = getCommonProcessor();
116
    final var rp = new InlineRProcessor( tpc, getResolvedMap() );
117
    return new RVariableProcessor( rp, getResolvedMap() );
118
  }
119
120
  protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
121
    final var tpc = getCommonProcessor();
122
    final var xmlp = new XmlProcessor( tpc, tab.getPath() );
123
    final var rp = new InlineRProcessor( xmlp, getResolvedMap() );
124
    return new RVariableProcessor( rp, getResolvedMap() );
125
  }
126
127
  private HTMLPreviewPane getPreviewPane() {
128
    return mPreviewPane;
129
  }
130
131
  /**
132
   * Returns the variable map of interpolated definitions.
133
   *
134
   * @return A map to help dereference variables.
135
   */
136
  private Map<String, String> getResolvedMap() {
137
    return mResolvedMap;
138
  }
139
140
  /**
141
   * Returns a processor common to all processors: markdown, caret position
142
   * token replacer, and an HTML preview renderer.
143
   *
144
   * @return Processors at the end of the processing chain.
145
   */
146
  private Processor<String> getCommonProcessor() {
147
    return mMarkdownProcessor;
148
  }
149
}
1150
A src/main/java/com/keenwrite/processors/RVariableProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
import com.keenwrite.sigils.RSigilOperator;
31
32
import java.util.HashMap;
33
import java.util.Map;
34
35
/**
36
 * Converts the keys of the resolved map from default form to R form, then
37
 * performs a substitution on the text. The default R variable syntax is
38
 * {@code v$tree$leaf}.
39
 */
40
public class RVariableProcessor extends DefinitionProcessor {
41
42
  public RVariableProcessor(
43
      final Processor<String> rp, final Map<String, String> map ) {
44
    super( rp, map );
45
  }
46
47
  /**
48
   * Returns the R-based version of the interpolated variable definitions.
49
   *
50
   * @return Variable names transmogrified from the default syntax to R syntax.
51
   */
52
  @Override
53
  protected Map<String, String> getDefinitions() {
54
    return toR( super.getDefinitions() );
55
  }
56
57
  /**
58
   * Converts the given map from regular variables to R variables.
59
   *
60
   * @param map Map of variable names to values.
61
   * @return Map of R variables.
62
   */
63
  private Map<String, String> toR( final Map<String, String> map ) {
64
    final var rMap = new HashMap<String, String>( map.size() );
65
66
    for( final var entry : map.entrySet() ) {
67
      final var key = entry.getKey();
68
      rMap.put( RSigilOperator.entoken( key ), toRValue( map.get( key ) ) );
69
    }
70
71
    return rMap;
72
  }
73
74
  private String toRValue( final String value ) {
75
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
76
  }
77
78
  /**
79
   * TODO: Make generic method for replacing text.
80
   *
81
   * @param haystack Search this string for the needle, must not be null.
82
   * @param needle   The character to find in the haystack.
83
   * @param thread   Replace the needle with this text, if the needle is found.
84
   * @return The haystack with the all instances of needle replaced with thread.
85
   */
86
  @SuppressWarnings("SameParameterValue")
87
  private String escape(
88
      final String haystack, final char needle, final String thread ) {
89
    int end = haystack.indexOf( needle );
90
91
    if( end < 0 ) {
92
      return haystack;
93
    }
94
95
    final int length = haystack.length();
96
    int start = 0;
97
98
    // Replace up to 32 occurrences before the string reallocates its buffer.
99
    final StringBuilder sb = new StringBuilder( length + 32 );
100
101
    while( end >= 0 ) {
102
      sb.append( haystack, start, end ).append( thread );
103
      start = end + 1;
104
      end = haystack.indexOf( needle, start );
105
    }
106
107
    return sb.append( haystack.substring( start ) ).toString();
108
  }
109
}
1110
A src/main/java/com/keenwrite/processors/XmlProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors;
29
30
import com.keenwrite.Services;
31
import com.keenwrite.service.Snitch;
32
import net.sf.saxon.TransformerFactoryImpl;
33
import net.sf.saxon.trans.XPathException;
34
35
import javax.xml.stream.XMLEventReader;
36
import javax.xml.stream.XMLInputFactory;
37
import javax.xml.stream.XMLStreamException;
38
import javax.xml.stream.events.ProcessingInstruction;
39
import javax.xml.stream.events.XMLEvent;
40
import javax.xml.transform.*;
41
import javax.xml.transform.stream.StreamResult;
42
import javax.xml.transform.stream.StreamSource;
43
import java.io.File;
44
import java.io.Reader;
45
import java.io.StringReader;
46
import java.io.StringWriter;
47
import java.nio.file.Path;
48
import java.nio.file.Paths;
49
50
import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
51
52
/**
53
 * Transforms an XML document. The XML document must have a stylesheet specified
54
 * as part of its processing instructions, such as:
55
 * <p>
56
 * {@code xml-stylesheet type="text/xsl" href="markdown.xsl"}
57
 * </p>
58
 * <p>
59
 * The XSL must transform the XML document into Markdown, or another format
60
 * recognized by the next link on the chain.
61
 * </p>
62
 */
63
public class XmlProcessor extends AbstractProcessor<String>
64
    implements ErrorListener {
65
66
  private final Snitch snitch = Services.load( Snitch.class );
67
68
  private XMLInputFactory xmlInputFactory;
69
  private TransformerFactory transformerFactory;
70
  private Transformer transformer;
71
72
  private Path path;
73
74
  /**
75
   * Constructs an XML processor that can transform an XML document into another
76
   * format based on the XSL file specified as a processing instruction. The
77
   * path must point to the directory where the XSL file is found, which implies
78
   * that they must be in the same directory.
79
   *
80
   * @param processor Next link in the processing chain.
81
   * @param path      The path to the XML file content to be processed.
82
   */
83
  public XmlProcessor( final Processor<String> processor, final Path path ) {
84
    super( processor );
85
    setPath( path );
86
  }
87
88
  /**
89
   * Transforms the given XML text into another form (typically Markdown).
90
   *
91
   * @param text The text to transform, can be empty, cannot be null.
92
   * @return The transformed text, or empty if text is empty.
93
   */
94
  @Override
95
  public String apply( final String text ) {
96
    try {
97
      return text.isEmpty() ? text : transform( text );
98
    } catch( final Exception ex ) {
99
      throw new RuntimeException( ex );
100
    }
101
  }
102
103
  /**
104
   * Performs an XSL transformation on the given XML text. The XML text must
105
   * have a processing instruction that points to the XSL template file to use
106
   * for the transformation.
107
   *
108
   * @param text The text to transform.
109
   * @return The transformed text.
110
   */
111
  private String transform( final String text ) throws Exception {
112
    // Extract the XML stylesheet processing instruction.
113
    final String template = getXsltFilename( text );
114
    final Path xsl = getXslPath( template );
115
116
    try(
117
        final StringWriter output = new StringWriter( text.length() );
118
        final StringReader input = new StringReader( text ) ) {
119
120
      // Listen for external file modification events.
121
      getSnitch().listen( xsl );
122
123
      getTransformer( xsl ).transform(
124
          new StreamSource( input ),
125
          new StreamResult( output )
126
      );
127
128
      return output.toString();
129
    }
130
  }
131
132
  /**
133
   * Returns an XSL transformer ready to transform an XML document using the
134
   * XSLT file specified by the given path. If the path is already known then
135
   * this will return the associated transformer.
136
   *
137
   * @param xsl The path to an XSLT file.
138
   * @return A transformer that will transform XML documents using the given
139
   * XSLT file.
140
   * @throws TransformerConfigurationException Could not instantiate the
141
   *                                           transformer.
142
   */
143
  private Transformer getTransformer( final Path xsl )
144
      throws TransformerConfigurationException {
145
    if( this.transformer == null ) {
146
      this.transformer = createTransformer( xsl );
147
    }
148
149
    return this.transformer;
150
  }
151
152
  /**
153
   * Creates a configured transformer ready to run.
154
   *
155
   * @param xsl The stylesheet to use for transforming XML documents.
156
   * @return The edited XML document transformed into another format (usually
157
   * markdown).
158
   * @throws TransformerConfigurationException Could not create the transformer.
159
   */
160
  protected Transformer createTransformer( final Path xsl )
161
      throws TransformerConfigurationException {
162
    final Source xslt = new StreamSource( xsl.toFile() );
163
164
    return getTransformerFactory().newTransformer( xslt );
165
  }
166
167
  private Path getXslPath( final String filename ) {
168
    final Path xmlPath = getPath();
169
    final File xmlDirectory = xmlPath.toFile().getParentFile();
170
171
    return Paths.get( xmlDirectory.getPath(), filename );
172
  }
173
174
  /**
175
   * Given XML text, this will use a StAX pull reader to obtain the XML
176
   * stylesheet processing instruction. This will throw a parse exception if the
177
   * href pseudo-attribute filename value cannot be found.
178
   *
179
   * @param xml The XML containing an xml-stylesheet processing instruction.
180
   * @return The href pseudo-attribute value.
181
   * @throws XMLStreamException Could not parse the XML file.
182
   */
183
  private String getXsltFilename( final String xml )
184
      throws XMLStreamException, XPathException {
185
186
    String result = "";
187
188
    try( final StringReader sr = new StringReader( xml ) ) {
189
      boolean found = false;
190
      int count = 0;
191
      final XMLEventReader reader = createXMLEventReader( sr );
192
193
      // If the processing instruction wasn't found in the first 10 lines,
194
      // fail fast. This should iterate twice through the loop.
195
      while( !found && reader.hasNext() && count++ < 10 ) {
196
        final XMLEvent event = reader.nextEvent();
197
198
        if( event.isProcessingInstruction() ) {
199
          final ProcessingInstruction pi = (ProcessingInstruction) event;
200
          final String target = pi.getTarget();
201
202
          if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
203
            result = getPseudoAttribute( pi.getData(), "href" );
204
            found = true;
205
          }
206
        }
207
      }
208
    }
209
210
    return result;
211
  }
212
213
  private XMLEventReader createXMLEventReader( final Reader reader )
214
      throws XMLStreamException {
215
    return getXMLInputFactory().createXMLEventReader( reader );
216
  }
217
218
  private synchronized XMLInputFactory getXMLInputFactory() {
219
    if( this.xmlInputFactory == null ) {
220
      this.xmlInputFactory = createXMLInputFactory();
221
    }
222
223
    return this.xmlInputFactory;
224
  }
225
226
  private XMLInputFactory createXMLInputFactory() {
227
    return XMLInputFactory.newInstance();
228
  }
229
230
  private synchronized TransformerFactory getTransformerFactory() {
231
    if( this.transformerFactory == null ) {
232
      this.transformerFactory = createTransformerFactory();
233
    }
234
235
    return this.transformerFactory;
236
  }
237
238
  /**
239
   * Returns a high-performance XSLT 2 transformation engine.
240
   *
241
   * @return An XSL transforming engine.
242
   */
243
  private TransformerFactory createTransformerFactory() {
244
    final TransformerFactory factory = new TransformerFactoryImpl();
245
246
    // Bubble problems up to the user interface, rather than standard error.
247
    factory.setErrorListener( this );
248
249
    return factory;
250
  }
251
252
  /**
253
   * Called when the XSL transformer issues a warning.
254
   *
255
   * @param ex The problem the transformer encountered.
256
   */
257
  @Override
258
  public void warning( final TransformerException ex ) {
259
    throw new RuntimeException( ex );
260
  }
261
262
  /**
263
   * Called when the XSL transformer issues an error.
264
   *
265
   * @param ex The problem the transformer encountered.
266
   */
267
  @Override
268
  public void error( final TransformerException ex ) {
269
    throw new RuntimeException( ex );
270
  }
271
272
  /**
273
   * Called when the XSL transformer issues a fatal error, which is probably
274
   * a bit over-dramatic a method name.
275
   *
276
   * @param ex The problem the transformer encountered.
277
   */
278
  @Override
279
  public void fatalError( final TransformerException ex ) {
280
    throw new RuntimeException( ex );
281
  }
282
283
  private void setPath( final Path path ) {
284
    this.path = path;
285
  }
286
287
  private Path getPath() {
288
    return this.path;
289
  }
290
291
  private Snitch getSnitch() {
292
    return this.snitch;
293
  }
294
}
1295
A src/main/java/com/keenwrite/processors/markdown/BlockExtension.java
1
package com.keenwrite.processors.markdown;
2
3
import com.vladsch.flexmark.ast.BlockQuote;
4
import com.vladsch.flexmark.ast.ListBlock;
5
import com.vladsch.flexmark.html.AttributeProvider;
6
import com.vladsch.flexmark.html.AttributeProviderFactory;
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
import static com.keenwrite.Constants.PARAGRAPH_ID_PREFIX;
17
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
18
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
19
import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
20
21
/**
22
 * Responsible for giving most block-level elements a unique identifier
23
 * attribute. The identifier is used to coordinate scrolling.
24
 */
25
public class BlockExtension implements HtmlRendererExtension {
26
  /**
27
   * Responsible for creating the id attribute. This class is instantiated
28
   * each time the document is rendered, thereby resetting the count to zero.
29
   */
30
  public static class IdAttributeProvider implements AttributeProvider {
31
    private int mCount;
32
33
    private static AttributeProviderFactory createFactory() {
34
      return new IndependentAttributeProviderFactory() {
35
        @Override
36
        public @NotNull AttributeProvider apply(
37
            @NotNull final LinkResolverContext context ) {
38
          return new IdAttributeProvider();
39
        }
40
      };
41
    }
42
43
    @Override
44
    public void setAttributes( @NotNull Node node,
45
                               @NotNull AttributablePart part,
46
                               @NotNull MutableAttributes attributes ) {
47
      // Blockquotes are troublesome because they can interleave blank lines
48
      // without having an equivalent blank line in the source document. That
49
      // is, in Markdown the > symbol on a line by itself will generate a blank
50
      // line in the resulting document; however, a > symbol in the text editor
51
      // does not count as a blank line. Resolving this issue is tricky.
52
      //
53
      // The CODE_CONTENT represents <code> embedded inside <pre>; both elements
54
      // enter this method as FencedCodeBlock, but only the <pre> must be
55
      // uniquely identified (because they are the same line in Markdown).
56
      //
57
      if( node instanceof Block &&
58
          !(node instanceof BlockQuote) &&
59
          !(node instanceof ListBlock) &&
60
          (part != CODE_CONTENT) ) {
61
        attributes.addValue( "id", PARAGRAPH_ID_PREFIX + mCount++ );
62
      }
63
    }
64
  }
65
66
  private BlockExtension() {
67
  }
68
69
  @Override
70
  public void extend( final Builder builder,
71
                      @NotNull final String rendererType ) {
72
    builder.attributeProviderFactory( IdAttributeProvider.createFactory() );
73
  }
74
75
  public static BlockExtension create() {
76
    return new BlockExtension();
77
  }
78
79
  @Override
80
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
81
  }
82
}
183
A src/main/java/com/keenwrite/processors/markdown/ImageLinkExtension.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.markdown;
29
30
import com.keenwrite.exceptions.MissingFileException;
31
import com.keenwrite.preferences.UserPreferences;
32
import com.vladsch.flexmark.ast.Image;
33
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
34
import com.vladsch.flexmark.html.LinkResolver;
35
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
36
import com.vladsch.flexmark.html.renderer.LinkStatus;
37
import com.vladsch.flexmark.html.renderer.ResolvedLink;
38
import com.vladsch.flexmark.util.ast.Node;
39
import com.vladsch.flexmark.util.data.MutableDataHolder;
40
import org.jetbrains.annotations.NotNull;
41
import org.renjin.repackaged.guava.base.Splitter;
42
43
import java.io.File;
44
import java.nio.file.Path;
45
46
import static com.keenwrite.StatusBarNotifier.alert;
47
import static com.keenwrite.util.ProtocolResolver.getProtocol;
48
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
49
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
50
import static java.lang.String.format;
51
import static org.apache.commons.io.FilenameUtils.getExtension;
52
import static org.apache.commons.io.FilenameUtils.removeExtension;
53
54
/**
55
 * Responsible for ensuring that images can be rendered relative to a path.
56
 * This allows images to be located virtually anywhere.
57
 */
58
public class ImageLinkExtension implements HtmlRendererExtension {
59
60
  /**
61
   * Creates an extension capable of using a relative path to embed images.
62
   *
63
   * @param path The {@link Path} to the file being edited; the parent path
64
   *             is the starting location of the relative image directory.
65
   * @return The new {@link ImageLinkExtension}, never {@code null}.
66
   */
67
  public static ImageLinkExtension create( @NotNull final Path path ) {
68
    return new ImageLinkExtension( path );
69
  }
70
71
  private class Factory extends IndependentLinkResolverFactory {
72
    @Override
73
    public @NotNull LinkResolver apply(
74
        @NotNull final LinkResolverBasicContext context ) {
75
      return new ImageLinkResolver();
76
    }
77
  }
78
79
  private class ImageLinkResolver implements LinkResolver {
80
    private final UserPreferences mUserPref = getUserPreferences();
81
    private final File mImagesUserPrefix = mUserPref.getImagesDirectory();
82
    private final String mImageExtensions = mUserPref.getImagesOrder();
83
84
    public ImageLinkResolver() {
85
    }
86
87
    /**
88
     * You can also set/clear/modify attributes through
89
     * {@link ResolvedLink#getAttributes()} and
90
     * {@link ResolvedLink#getNonNullAttributes()}.
91
     */
92
    @NotNull
93
    @Override
94
    public ResolvedLink resolveLink(
95
        @NotNull final Node node,
96
        @NotNull final LinkResolverBasicContext context,
97
        @NotNull final ResolvedLink link ) {
98
      return node instanceof Image ? resolve( link ) : link;
99
    }
100
101
    private ResolvedLink resolve( final ResolvedLink link ) {
102
      var url = link.getUrl();
103
      final var protocol = getProtocol( url );
104
105
      try {
106
        if( protocol.isHttp() ) {
107
          return valid( link, url );
108
        }
109
      } catch( final Exception ignored ) {
110
        // Try to resolve the image path, dynamically.
111
      }
112
113
      try {
114
        final Path imagePrefix = getImagePrefix().toPath();
115
116
        // Path to the file being edited.
117
        Path editPath = getEditPath();
118
119
        // If there is no parent path to the file, it means the file has not
120
        // been saved. Default to using the value from the user's preferences.
121
        // The user's preferences will be defaulted to a the application's
122
        // starting directory.
123
        editPath = editPath == null
124
            ? imagePrefix
125
            : Path.of( editPath.toString(), imagePrefix.toString() );
126
127
        final var urlExt = getExtension( url );
128
        url = removeExtension( url );
129
130
        final var suffixes = urlExt + ' ' + getImageExtensions();
131
        final var imagePathPrefix = Path.of( editPath.toString(), url );
132
        var suffix = ".*";
133
        boolean missing = true;
134
135
        // Iterate over the user's preferred image file type extensions.
136
        for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) {
137
          final String imagePath = format( "%s.%s", imagePathPrefix, ext );
138
          final File file = new File( imagePath );
139
140
          if( file.exists() ) {
141
            url = file.toString();
142
            missing = false;
143
            break;
144
          }
145
          else if( !urlExt.isBlank() ) {
146
            // The file is missing because the user specified a prefix.
147
            suffix = urlExt;
148
            break;
149
          }
150
        }
151
152
        if( missing ) {
153
          throw new MissingFileException( imagePathPrefix + suffix );
154
        }
155
156
        if( protocol.isFile() ) {
157
          url = "file://" + url;
158
        }
159
160
        return valid( link, url );
161
      } catch( final Exception ex ) {
162
        alert( ex );
163
      }
164
165
      return link;
166
    }
167
168
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
169
      return link.withStatus( LinkStatus.VALID ).withUrl( url );
170
    }
171
172
    private File getImagePrefix() {
173
      return mImagesUserPrefix;
174
    }
175
176
    private String getImageExtensions() {
177
      return mImageExtensions;
178
    }
179
180
    private Path getEditPath() {
181
      return mPath.getParent();
182
    }
183
  }
184
185
  private final Path mPath;
186
187
  private ImageLinkExtension( @NotNull final Path path ) {
188
    mPath = path;
189
  }
190
191
  @Override
192
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
193
  }
194
195
  @Override
196
  public void extend( @NotNull final Builder builder,
197
                      @NotNull final String rendererType ) {
198
    builder.linkResolverFactory( new Factory() );
199
  }
200
201
  private UserPreferences getUserPreferences() {
202
    return UserPreferences.getInstance();
203
  }
204
}
1205
A src/main/java/com/keenwrite/processors/markdown/LigatureExtension.java
1
package com.keenwrite.processors.markdown;
2
3
import com.vladsch.flexmark.ast.Text;
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.html.renderer.NodeRenderer;
6
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
7
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
9
import com.vladsch.flexmark.util.ast.TextCollectingVisitor;
10
import com.vladsch.flexmark.util.data.DataHolder;
11
import com.vladsch.flexmark.util.data.MutableDataHolder;
12
import org.jetbrains.annotations.NotNull;
13
import org.jetbrains.annotations.Nullable;
14
15
import java.util.LinkedHashMap;
16
import java.util.Map;
17
import java.util.Set;
18
19
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
20
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
21
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
22
23
/**
24
 * Responsible for substituting multi-codepoint glyphs with single codepoint
25
 * glyphs. The text is adorned with ligatures prior to rendering as HTML.
26
 * This requires a font that supports ligatures.
27
 * <p>
28
 * TODO: #81 -- I18N
29
 * </p>
30
 */
31
public class LigatureExtension implements HtmlRendererExtension {
32
  /**
33
   * Retain insertion order so that ligature substitution uses longer ligatures
34
   * ahead of shorter ligatures. The word "ruffian" should use the "ffi"
35
   * ligature, not the "ff" ligature.
36
   */
37
  private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
38
39
  static {
40
    LIGATURES.put( "ffi", "\uFB03" );
41
    LIGATURES.put( "ffl", "\uFB04" );
42
    LIGATURES.put( "ff", "\uFB00" );
43
    LIGATURES.put( "fi", "\uFB01" );
44
    LIGATURES.put( "fl", "\uFB02" );
45
    LIGATURES.put( "ft", "\uFB05" );
46
    LIGATURES.put( "AE", "\u00C6" );
47
    LIGATURES.put( "OE", "\u0152" );
48
//      "ae", "\u00E6",
49
//      "oe", "\u0153",
50
  }
51
52
  private static class LigatureRenderer implements NodeRenderer {
53
    private final TextCollectingVisitor mVisitor = new TextCollectingVisitor();
54
55
    @SuppressWarnings("unused")
56
    public LigatureRenderer( final DataHolder options ) {
57
    }
58
59
    @Override
60
    public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
61
      return Set.of( new NodeRenderingHandler<>(
62
          Text.class, LigatureRenderer.this::render ) );
63
    }
64
65
    /**
66
     * This will pick the fastest string replacement algorithm based on the
67
     * text length. The insertion order of the {@link #LIGATURES} is
68
     * important to give precedence to longer ligatures.
69
     *
70
     * @param textNode The text node containing text to replace with ligatures.
71
     * @param context  Not used.
72
     * @param html     Where to write the text adorned with ligatures.
73
     */
74
    private void render(
75
        @NotNull final Text textNode,
76
        @NotNull final NodeRendererContext context,
77
        @NotNull final HtmlWriter html ) {
78
      final var text = mVisitor.collectAndGetText( textNode );
79
      html.text( replace( text, LIGATURES ) );
80
    }
81
  }
82
83
  private static class Factory implements NodeRendererFactory {
84
    @NotNull
85
    @Override
86
    public NodeRenderer apply( @NotNull DataHolder options ) {
87
      return new LigatureRenderer( options );
88
    }
89
  }
90
91
  private LigatureExtension() {
92
  }
93
94
  @Override
95
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
96
  }
97
98
  @Override
99
  public void extend( @NotNull final Builder builder,
100
                      @NotNull final String rendererType ) {
101
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
102
      builder.nodeRendererFactory( new Factory() );
103
    }
104
  }
105
106
  public static LigatureExtension create() {
107
    return new LigatureExtension();
108
  }
109
}
1110
A src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.markdown;
29
30
import com.keenwrite.processors.AbstractProcessor;
31
import com.keenwrite.processors.Processor;
32
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
33
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
34
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
35
import com.vladsch.flexmark.ext.tables.TablesExtension;
36
import com.vladsch.flexmark.ext.typographic.TypographicExtension;
37
import com.vladsch.flexmark.html.HtmlRenderer;
38
import com.vladsch.flexmark.parser.Parser;
39
import com.vladsch.flexmark.util.ast.IParse;
40
import com.vladsch.flexmark.util.ast.Node;
41
import com.vladsch.flexmark.util.misc.Extension;
42
43
import java.nio.file.Path;
44
import java.util.ArrayList;
45
import java.util.Collection;
46
47
import static com.keenwrite.Constants.USER_DIRECTORY;
48
49
/**
50
 * Responsible for parsing a Markdown document and rendering it as HTML.
51
 */
52
public class MarkdownProcessor extends AbstractProcessor<String> {
53
54
  private final HtmlRenderer mRenderer;
55
  private final IParse mParser;
56
57
  public MarkdownProcessor(
58
      final Processor<String> successor ) {
59
    this( successor, Path.of( USER_DIRECTORY ) );
60
  }
61
62
  /**
63
   * Constructs a new Markdown processor that can create HTML documents.
64
   *
65
   * @param successor Usually the HTML Preview Processor.
66
   */
67
  public MarkdownProcessor(
68
      final Processor<String> successor, final Path path ) {
69
    super( successor );
70
71
    // Standard extensions
72
    final Collection<Extension> extensions = new ArrayList<>();
73
    extensions.add( DefinitionExtension.create() );
74
    extensions.add( StrikethroughSubscriptExtension.create() );
75
    extensions.add( SuperscriptExtension.create() );
76
    extensions.add( TablesExtension.create() );
77
    extensions.add( TypographicExtension.create() );
78
79
    // Allows referencing image files via relative paths and dynamic file types.
80
    extensions.add( ImageLinkExtension.create( path ) );
81
    extensions.add( BlockExtension.create() );
82
    extensions.add( TeXExtension.create() );
83
84
    // TODO: https://github.com/FAlthausen/Vollkorn-Typeface/issues/38
85
    // TODO: Uncomment when Vollkorn ligatures are fixed.
86
    // extensions.add( LigatureExtension.create() );
87
88
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
89
    mParser = Parser.builder()
90
                    .extensions( extensions )
91
                    .build();
92
  }
93
94
  /**
95
   * Converts the given Markdown string into HTML, without the doctype, html,
96
   * head, and body tags.
97
   *
98
   * @param markdown The string to convert from Markdown to HTML.
99
   * @return The HTML representation of the Markdown document.
100
   */
101
  @Override
102
  public String apply( final String markdown ) {
103
    return toHtml( markdown );
104
  }
105
106
  /**
107
   * Returns the AST in the form of a node for the given markdown document. This
108
   * can be used, for example, to determine if a hyperlink exists inside of a
109
   * paragraph.
110
   *
111
   * @param markdown The markdown to convert into an AST.
112
   * @return The markdown AST for the given text (usually a paragraph).
113
   */
114
  public Node toNode( final String markdown ) {
115
    return parse( markdown );
116
  }
117
118
  /**
119
   * Helper method to create an AST given some markdown.
120
   *
121
   * @param markdown The markdown to parse.
122
   * @return The root node of the markdown tree.
123
   */
124
  private Node parse( final String markdown ) {
125
    return getParser().parse( markdown );
126
  }
127
128
  /**
129
   * Converts a string of markdown into HTML.
130
   *
131
   * @param markdown The markdown text to convert to HTML, must not be null.
132
   * @return The markdown rendered as an HTML document.
133
   */
134
  private String toHtml( final String markdown ) {
135
    return getRenderer().render( parse( markdown ) );
136
  }
137
138
  /**
139
   * Creates the Markdown document processor.
140
   *
141
   * @return A Parser that can build an abstract syntax tree.
142
   */
143
  private IParse getParser() {
144
    return mParser;
145
  }
146
147
  private HtmlRenderer getRenderer() {
148
    return mRenderer;
149
  }
150
}
1151
A src/main/java/com/keenwrite/processors/markdown/TeXExtension.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.markdown;
29
30
import com.keenwrite.processors.markdown.tex.TeXInlineDelimiterProcessor;
31
import com.keenwrite.processors.markdown.tex.TeXNodeRenderer;
32
import com.vladsch.flexmark.html.HtmlRenderer;
33
import com.vladsch.flexmark.parser.Parser;
34
import com.vladsch.flexmark.util.data.MutableDataHolder;
35
import org.jetbrains.annotations.NotNull;
36
37
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
38
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
39
40
/**
41
 * Responsible for wrapping delimited TeX code in Markdown into an XML element
42
 * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
43
 * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
44
 * is responsible for converting the TeX code for display. This avoids inserting
45
 * SVG code into the Markdown document, which the parser would then have to
46
 * iterate---a <em>very</em> wasteful operation that impacts front-end
47
 * performance.
48
 */
49
public class TeXExtension implements ParserExtension, HtmlRendererExtension {
50
  /**
51
   * Creates an extension capable of handling delimited TeX code in Markdown.
52
   *
53
   * @return The new {@link TeXExtension}, never {@code null}.
54
   */
55
  public static TeXExtension create() {
56
    return new TeXExtension();
57
  }
58
59
  /**
60
   * Force using the {@link #create()} method for consistency.
61
   */
62
  private TeXExtension() {
63
  }
64
65
  /**
66
   * Adds the TeX extension for HTML document export types.
67
   *
68
   * @param builder      The document builder.
69
   * @param rendererType Indicates the document type to be built.
70
   */
71
  @Override
72
  public void extend( @NotNull final HtmlRenderer.Builder builder,
73
                      @NotNull final String rendererType ) {
74
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
75
      builder.nodeRendererFactory( new TeXNodeRenderer.Factory() );
76
    }
77
  }
78
79
  @Override
80
  public void extend( final Parser.Builder builder ) {
81
    builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() );
82
  }
83
84
  @Override
85
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
86
  }
87
88
  @Override
89
  public void parserOptions( final MutableDataHolder options ) {
90
  }
91
}
192
A src/main/java/com/keenwrite/processors/markdown/tex/TeXInlineDelimiterProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.markdown.tex;
29
30
import com.vladsch.flexmark.parser.InlineParser;
31
import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
32
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
33
import com.vladsch.flexmark.parser.delimiter.DelimiterRun;
34
import com.vladsch.flexmark.util.ast.Node;
35
36
public class TeXInlineDelimiterProcessor implements DelimiterProcessor {
37
38
  @Override
39
  public void process( final Delimiter opener, final Delimiter closer,
40
                       final int delimitersUsed ) {
41
    final var node = new TeXNode();
42
    opener.moveNodesBetweenDelimitersTo(node, closer);
43
  }
44
45
  @Override
46
  public char getOpeningCharacter() {
47
    return '$';
48
  }
49
50
  @Override
51
  public char getClosingCharacter() {
52
    return '$';
53
  }
54
55
  @Override
56
  public int getMinLength() {
57
    return 1;
58
  }
59
60
  /**
61
   * Allow for $ or $$.
62
   *
63
   * @param opener One or more opening delimiter characters.
64
   * @param closer One or more closing delimiter characters.
65
   * @return The number of delimiters to use to determine whether a valid
66
   * opening delimiter expression is found.
67
   */
68
  @Override
69
  public int getDelimiterUse(
70
      final DelimiterRun opener, final DelimiterRun closer ) {
71
    return 1;
72
  }
73
74
  @Override
75
  public boolean canBeOpener( final String before,
76
                              final String after,
77
                              final boolean leftFlanking,
78
                              final boolean rightFlanking,
79
                              final boolean beforeIsPunctuation,
80
                              final boolean afterIsPunctuation,
81
                              final boolean beforeIsWhitespace,
82
                              final boolean afterIsWhiteSpace ) {
83
    return leftFlanking;
84
  }
85
86
  @Override
87
  public boolean canBeCloser( final String before,
88
                              final String after,
89
                              final boolean leftFlanking,
90
                              final boolean rightFlanking,
91
                              final boolean beforeIsPunctuation,
92
                              final boolean afterIsPunctuation,
93
                              final boolean beforeIsWhitespace,
94
                              final boolean afterIsWhiteSpace ) {
95
    return rightFlanking;
96
  }
97
98
  @Override
99
  public Node unmatchedDelimiterNode(
100
      final InlineParser inlineParser, final DelimiterRun delimiter ) {
101
    return null;
102
  }
103
104
  @Override
105
  public boolean skipNonOpenerCloser() {
106
    return false;
107
  }
108
}
1109
A src/main/java/com/keenwrite/processors/markdown/tex/TeXNode.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.markdown.tex;
29
30
import com.vladsch.flexmark.ast.DelimitedNodeImpl;
31
32
public class TeXNode extends DelimitedNodeImpl {
33
  /**
34
   * TeX expression wrapped in a {@code <tex>} element.
35
   */
36
  public static final String HTML_TEX = "tex";
37
38
  public TeXNode() {
39
  }
40
}
141
A src/main/java/com/keenwrite/processors/markdown/tex/TeXNodeRenderer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.markdown.tex;
29
30
import com.vladsch.flexmark.html.HtmlWriter;
31
import com.vladsch.flexmark.html.renderer.NodeRenderer;
32
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
33
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
34
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
35
import com.vladsch.flexmark.util.data.DataHolder;
36
import org.jetbrains.annotations.NotNull;
37
import org.jetbrains.annotations.Nullable;
38
39
import java.util.Set;
40
41
import static com.keenwrite.processors.markdown.tex.TeXNode.HTML_TEX;
42
43
public class TeXNodeRenderer implements NodeRenderer {
44
45
  public static class Factory implements NodeRendererFactory {
46
    @NotNull
47
    @Override
48
    public NodeRenderer apply( @NotNull DataHolder options ) {
49
      return new TeXNodeRenderer();
50
    }
51
  }
52
53
  @Override
54
  public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
55
    return Set.of( new NodeRenderingHandler<>( TeXNode.class, this::render ) );
56
  }
57
58
  private void render( final TeXNode node,
59
                       final NodeRendererContext context,
60
                       final HtmlWriter html ) {
61
    html.tag( HTML_TEX );
62
    html.raw( node.getText() );
63
    html.closeTag( HTML_TEX );
64
  }
65
}
166
A src/main/java/com/keenwrite/processors/text/AbstractTextReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Responsible for common behaviour across all text replacer implementations.
34
 */
35
public abstract class AbstractTextReplacer implements TextReplacer {
36
37
  /**
38
   * Default (empty) constructor.
39
   */
40
  protected AbstractTextReplacer() {
41
  }
42
43
  protected String[] keys( final Map<String, String> map ) {
44
    return map.keySet().toArray( new String[ 0 ] );
45
  }
46
47
  protected String[] values( final Map<String, String> map ) {
48
    return map.values().toArray( new String[ 0 ] );
49
  }
50
}
151
A src/main/java/com/keenwrite/processors/text/AhoCorasickReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.text;
29
30
import java.util.Map;
31
import org.ahocorasick.trie.Emit;
32
import org.ahocorasick.trie.Trie.TrieBuilder;
33
import static org.ahocorasick.trie.Trie.builder;
34
35
/**
36
 * Replaces text using an Aho-Corasick algorithm.
37
 */
38
public class AhoCorasickReplacer extends AbstractTextReplacer {
39
40
  /**
41
   * Default (empty) constructor.
42
   */
43
  protected AhoCorasickReplacer() {
44
  }
45
46
  @Override
47
  public String replace( final String text, final Map<String, String> map ) {
48
    // Create a buffer sufficiently large that re-allocations are minimized.
49
    final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
50
51
    // The TrieBuilder should only match whole words and ignore overlaps (there
52
    // shouldn't be any).
53
    final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps();
54
55
    for( final String key : keys( map ) ) {
56
      builder.addKeyword( key );
57
    }
58
59
    int index = 0;
60
61
    // Replace all instances with dereferenced variables.
62
    for( final Emit emit : builder.build().parseText( text ) ) {
63
      sb.append( text, index, emit.getStart() );
64
      sb.append( map.get( emit.getKeyword() ) );
65
      index = emit.getEnd() + 1;
66
    }
67
68
    // Add the remainder of the string (contains no more matches).
69
    sb.append( text.substring( index ) );
70
71
    return sb.toString();
72
  }
73
}
174
A src/main/java/com/keenwrite/processors/text/StringUtilsReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.text;
29
30
import java.util.Map;
31
32
import static org.apache.commons.lang3.StringUtils.replaceEach;
33
34
/**
35
 * Replaces text using Apache's StringUtils.replaceEach method.
36
 */
37
public class StringUtilsReplacer extends AbstractTextReplacer {
38
39
  /**
40
   * Default (empty) constructor.
41
   */
42
  protected StringUtilsReplacer() {
43
  }
44
45
  @Override
46
  public String replace( final String text, final Map<String, String> map ) {
47
    return replaceEach( text, keys( map ), values( map ) );
48
  }
49
}
150
A src/main/java/com/keenwrite/processors/text/TextReplacementFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Used to generate a class capable of efficiently replacing variable
34
 * definitions with their values.
35
 */
36
public final class TextReplacementFactory {
37
38
  private static final TextReplacer APACHE = new StringUtilsReplacer();
39
  private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
40
41
  /**
42
   * Returns a text search/replacement instance that is reasonably optimal for
43
   * the given length of text.
44
   *
45
   * @param length The length of text that requires some search and replacing.
46
   * @return A class that can search and replace text with utmost expediency.
47
   */
48
  public static TextReplacer getTextReplacer( final int length ) {
49
    // After about 1,500 characters, the StringUtils implementation is less
50
    // performant than the Aho-Corsick implementation.
51
    //
52
    // See http://stackoverflow.com/a/40836618/59087
53
    return length < 1500 ? APACHE : AHO_CORASICK;
54
  }
55
56
  /**
57
   * Convenience method to instantiate a suitable text replacer algorithm and
58
   * perform a replacement using the given map. At this point, the values should
59
   * be already dereferenced and ready to be substituted verbatim; any
60
   * recursively defined values must have been interpolated previously.
61
   *
62
   * @param text The text containing zero or more variables to replace.
63
   * @param map  The map of variables to their dereferenced values.
64
   * @return The text with all variables replaced.
65
   */
66
  public static String replace(
67
      final String text, final Map<String, String> map ) {
68
    return getTextReplacer( text.length() ).replace( text, map );
69
  }
70
}
171
A src/main/java/com/keenwrite/processors/text/TextReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Defines the ability to replace text given a set of keys and values.
34
 */
35
public interface TextReplacer {
36
37
  /**
38
   * Searches through the given text for any of the keys given in the map and
39
   * replaces the keys that appear in the text with the key's corresponding
40
   * value.
41
   *
42
   * @param text The text that contains zero or more keys.
43
   * @param map  The set of keys mapped to replacement values.
44
   * @return The given text with all keys replaced with corresponding values.
45
   */
46
  String replace( String text, Map<String, String> map );
47
}
148
A src/main/java/com/keenwrite/service/Options.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service;
29
30
import com.dlsc.preferencesfx.PreferencesFx;
31
32
import java.util.prefs.BackingStoreException;
33
import java.util.prefs.Preferences;
34
35
/**
36
 * Responsible for persisting options that are safe to load before the UI
37
 * is shown. This can include items like window dimensions, last file
38
 * opened, split pane locations, and more. This cannot be used to persist
39
 * options that are user-controlled (i.e., all options available through
40
 * {@link PreferencesFx}).
41
 */
42
public interface Options extends Service {
43
44
  /**
45
   * Returns the {@link Preferences} that persist settings that cannot
46
   * be configured via the user interface.
47
   *
48
   * @return A valid {@link Preferences} instance, never {@code null}.
49
   */
50
  Preferences getState();
51
52
  /**
53
   * Stores the key and value into the user preferences to be loaded the next
54
   * time the application is launched.
55
   *
56
   * @param key   Name of the key to persist along with its value.
57
   * @param value Value to associate with the key.
58
   * @throws BackingStoreException Could not persist the change.
59
   */
60
  void put( String key, String value ) throws BackingStoreException;
61
62
  /**
63
   * Retrieves the value for a key in the user preferences.
64
   *
65
   * @param key          Retrieve the value of this key.
66
   * @param defaultValue The value to return in the event that the given key has
67
   *                     no associated value.
68
   * @return The value associated with the key.
69
   */
70
  String get( String key, String defaultValue );
71
72
  /**
73
   * Retrieves the value for a key in the user preferences. This will return
74
   * the empty string if the value cannot be found.
75
   *
76
   * @param key The key to find in the preferences.
77
   * @return A non-null, possibly empty value for the key.
78
   */
79
  String get( String key );
80
}
181
A src/main/java/com/keenwrite/service/Service.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service;
29
30
/**
31
 * All services inherit from this one.
32
 */
33
public interface Service {
34
}
135
A src/main/java/com/keenwrite/service/Settings.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service;
29
30
import java.util.Iterator;
31
import java.util.List;
32
33
/**
34
 * Defines how settings and options can be retrieved.
35
 */
36
public interface Settings extends Service {
37
38
  /**
39
   * Returns a setting property or its default value.
40
   *
41
   * @param property     The property key name to obtain its value.
42
   * @param defaultValue The default value to return iff the property cannot be
43
   *                     found.
44
   * @return The property value for the given property key.
45
   */
46
  String getSetting( String property, String defaultValue );
47
48
  /**
49
   * Returns a setting property or its default value.
50
   *
51
   * @param property     The property key name to obtain its value.
52
   * @param defaultValue The default value to return iff the property cannot be
53
   *                     found.
54
   * @return The property value for the given property key.
55
   */
56
  int getSetting( String property, int defaultValue );
57
58
  /**
59
   * Returns a list of property names that begin with the given prefix. The
60
   * prefix is included in any matching results. This will return keys that
61
   * either match the prefix or start with the prefix followed by a dot ('.').
62
   * For example, a prefix value of <code>the.property.name</code> will likely
63
   * return the expected results, but <code>the.property.name.</code> (note the
64
   * extraneous period) will probably not.
65
   *
66
   * @param prefix The prefix to compare against each property name.
67
   * @return The list of property names that have the given prefix.
68
   */
69
  Iterator<String> getKeys( final String prefix );
70
71
  /**
72
   * Convert the generic list of property objects into strings.
73
   *
74
   * @param property The property value to coerce.
75
   * @param defaults The defaults values to use should the property be unset.
76
   * @return The list of properties coerced from objects to strings.
77
   */
78
  List<String> getStringSettingList( String property, List<String> defaults );
79
80
  /**
81
   * Converts the generic list of property objects into strings.
82
   *
83
   * @param property The property value to coerce.
84
   * @return The list of properties coerced from objects to strings.
85
   */
86
  List<String> getStringSettingList( String property );
87
}
188
A src/main/java/com/keenwrite/service/Snitch.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service;
29
30
import java.io.IOException;
31
import java.nio.file.Path;
32
import java.util.Observer;
33
34
/**
35
 * Listens for changes to file system files and directories.
36
 */
37
public interface Snitch extends Service, Runnable {
38
39
  /**
40
   * Adds an observer to the set of observers for this object, provided that it
41
   * is not the same as some observer already in the set. The order in which
42
   * notifications will be delivered to multiple observers is not specified.
43
   *
44
   * @param o The object to receive changed events for when monitored files
45
   *          are changed.
46
   */
47
  void addObserver( Observer o );
48
49
  /**
50
   * Listens for changes to the path. If the path specifies a file, then only
51
   * notifications pertaining to that file are sent. Otherwise, change events
52
   * for the directory that contains the file are sent. This method must allow
53
   * for multiple calls to the same file without incurring additional listeners
54
   * or events.
55
   *
56
   * @param file Send notifications when this file changes, can be null.
57
   * @throws IOException Couldn't create a watcher for the given file.
58
   */
59
  void listen( Path file ) throws IOException;
60
61
  /**
62
   * Removes the given file from the notifications list.
63
   *
64
   * @param file The file to stop monitoring for any changes, can be null.
65
   */
66
  void ignore( Path file );
67
68
  /**
69
   * Stop listening for events.
70
   */
71
  void stop();
72
}
173
A src/main/java/com/keenwrite/service/events/Notification.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service.events;
29
30
/**
31
 * Represents a message that contains a title and content.
32
 */
33
public interface Notification {
34
35
  /**
36
   * Alert title.
37
   *
38
   * @return A non-null string to use as alert message title.
39
   */
40
  String getTitle();
41
42
  /**
43
   * Alert message content.
44
   *
45
   * @return A non-null string that contains information for the user.
46
   */
47
  String getContent();
48
}
149
A src/main/java/com/keenwrite/service/events/Notifier.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service.events;
29
30
import javafx.scene.control.Alert;
31
import javafx.scene.control.ButtonType;
32
import javafx.stage.Window;
33
34
/**
35
 * Provides the application with a uniform way to notify the user of events.
36
 */
37
public interface Notifier {
38
39
  ButtonType YES = ButtonType.YES;
40
  ButtonType NO = ButtonType.NO;
41
  ButtonType CANCEL = ButtonType.CANCEL;
42
43
  /**
44
   * Constructs a default alert message text for a modal alert dialog.
45
   *
46
   * @param title   The dialog box message title.
47
   * @param message The dialog box message content (needs formatting).
48
   * @param args    The arguments to the message content that must be formatted.
49
   * @return The message suitable for building a modal alert dialog.
50
   */
51
  Notification createNotification(
52
      String title,
53
      String message,
54
      Object... args );
55
56
  /**
57
   * Creates an alert of alert type error with a message showing the cause of
58
   * the error.
59
   *
60
   * @param parent  Dialog box owner (for modal purposes).
61
   * @param message The error message, title, and possibly more details.
62
   * @return A modal alert dialog box ready to display using showAndWait.
63
   */
64
  Alert createError( Window parent, Notification message );
65
66
  /**
67
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
68
   *
69
   * @param parent  Dialog box owner (for modal purposes).
70
   * @param message The message, title, and possibly more details.
71
   * @return A modal alert dialog box ready to display using showAndWait.
72
   */
73
  Alert createConfirmation( Window parent, Notification message );
74
}
175
A src/main/java/com/keenwrite/service/events/impl/ButtonOrderPane.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service.events.impl;
29
30
import javafx.scene.Node;
31
import javafx.scene.control.ButtonBar;
32
import javafx.scene.control.DialogPane;
33
34
import static com.keenwrite.Constants.SETTINGS;
35
import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
36
37
/**
38
 * Ensures a consistent button order for alert dialogs across platforms (because
39
 * the default button order on Linux defies all logic).
40
 */
41
public class ButtonOrderPane extends DialogPane {
42
43
  @Override
44
  protected Node createButtonBar() {
45
    final var node = (ButtonBar) super.createButtonBar();
46
    node.setButtonOrder( getButtonOrder() );
47
    return node;
48
  }
49
50
  private String getButtonOrder() {
51
    return getSetting( "dialog.alert.button.order.windows",
52
                       BUTTON_ORDER_WINDOWS );
53
  }
54
55
  @SuppressWarnings("SameParameterValue")
56
  private String getSetting( final String key, final String defaultValue ) {
57
    return SETTINGS.getSetting( key, defaultValue );
58
  }
59
}
160
A src/main/java/com/keenwrite/service/events/impl/DefaultNotification.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service.events.impl;
29
30
import com.keenwrite.service.events.Notification;
31
32
import java.text.MessageFormat;
33
34
/**
35
 * Responsible for alerting the user to prominent information.
36
 */
37
public class DefaultNotification implements Notification {
38
39
  private final String title;
40
  private final String content;
41
42
  /**
43
   * Constructs default message text for a notification.
44
   *
45
   * @param title   The message title.
46
   * @param message The message content (needs formatting).
47
   * @param args    The arguments to the message content that must be formatted.
48
   */
49
  public DefaultNotification(
50
      final String title,
51
      final String message,
52
      final Object... args ) {
53
    this.title = title;
54
    this.content = MessageFormat.format( message, args );
55
  }
56
57
  @Override
58
  public String getTitle() {
59
    return this.title;
60
  }
61
62
  @Override
63
  public String getContent() {
64
    return this.content;
65
  }
66
}
167
A src/main/java/com/keenwrite/service/events/impl/DefaultNotifier.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service.events.impl;
29
30
import com.keenwrite.service.events.Notification;
31
import com.keenwrite.service.events.Notifier;
32
import javafx.scene.control.Alert;
33
import javafx.scene.control.Alert.AlertType;
34
import javafx.stage.Window;
35
36
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
37
import static javafx.scene.control.Alert.AlertType.ERROR;
38
39
/**
40
 * Provides the ability to notify the user of events that need attention,
41
 * such as prompting the user to confirm closing when there are unsaved changes.
42
 */
43
public final class DefaultNotifier implements Notifier {
44
45
  /**
46
   * Contains all the information that the user needs to know about a problem.
47
   *
48
   * @param title   The context for the message.
49
   * @param message The message content (formatted with the given args).
50
   * @param args    Parameters for the message content.
51
   * @return A notification instance, never null.
52
   */
53
  @Override
54
  public Notification createNotification(
55
      final String title,
56
      final String message,
57
      final Object... args ) {
58
    return new DefaultNotification( title, message, args );
59
  }
60
61
  private Alert createAlertDialog(
62
      final Window parent,
63
      final AlertType alertType,
64
      final Notification message ) {
65
66
    final Alert alert = new Alert( alertType );
67
68
    alert.setDialogPane( new ButtonOrderPane() );
69
    alert.setTitle( message.getTitle() );
70
    alert.setHeaderText( null );
71
    alert.setContentText( message.getContent() );
72
    alert.initOwner( parent );
73
74
    return alert;
75
  }
76
77
  @Override
78
  public Alert createConfirmation( final Window parent,
79
                                   final Notification message ) {
80
    final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
81
82
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
83
84
    return alert;
85
  }
86
87
  @Override
88
  public Alert createError( final Window parent, final Notification message ) {
89
    return createAlertDialog( parent, ERROR, message );
90
  }
91
}
192
A src/main/java/com/keenwrite/service/impl/DefaultOptions.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.service.impl;
28
29
import com.keenwrite.service.Options;
30
31
import java.util.prefs.BackingStoreException;
32
import java.util.prefs.Preferences;
33
34
import static com.keenwrite.Constants.PREFS_ROOT;
35
import static com.keenwrite.Constants.PREFS_STATE;
36
import static java.util.prefs.Preferences.userRoot;
37
38
/**
39
 * Persistent options user can change at runtime.
40
 */
41
public class DefaultOptions implements Options {
42
  public DefaultOptions() {
43
  }
44
45
  /**
46
   * This will throw IllegalArgumentException if the value exceeds the maximum
47
   * preferences value length.
48
   *
49
   * @param key   The name of the key to associate with the value.
50
   * @param value The value to persist.
51
   * @throws BackingStoreException New value not persisted.
52
   */
53
  @Override
54
  public void put( final String key, final String value )
55
      throws BackingStoreException {
56
    getState().put( key, value );
57
    getState().flush();
58
  }
59
60
  @Override
61
  public String get( final String key, final String value ) {
62
    return getState().get( key, value );
63
  }
64
65
  @Override
66
  public String get( final String key ) {
67
    return get( key, "" );
68
  }
69
70
  private Preferences getRootPreferences() {
71
    return userRoot().node( PREFS_ROOT );
72
  }
73
74
  @Override
75
  public Preferences getState() {
76
    return getRootPreferences().node( PREFS_STATE );
77
  }
78
}
179
A src/main/java/com/keenwrite/service/impl/DefaultSettings.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service.impl;
29
30
import com.keenwrite.service.Settings;
31
import org.apache.commons.configuration2.PropertiesConfiguration;
32
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
33
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
34
35
import java.io.InputStreamReader;
36
import java.net.URL;
37
import java.nio.charset.Charset;
38
import java.util.Iterator;
39
import java.util.List;
40
41
import static com.keenwrite.Constants.PATH_PROPERTIES_SETTINGS;
42
43
/**
44
 * Responsible for loading settings that help avoid hard-coded assumptions.
45
 */
46
public class DefaultSettings implements Settings {
47
48
  private static final char VALUE_SEPARATOR = ',';
49
50
  private PropertiesConfiguration mProperties;
51
52
  public DefaultSettings() {
53
    setProperties( createProperties() );
54
  }
55
56
  /**
57
   * Returns the value of a string property.
58
   *
59
   * @param property     The property key.
60
   * @param defaultValue The value to return if no property key has been set.
61
   * @return The property key value, or defaultValue when no key found.
62
   */
63
  @Override
64
  public String getSetting( final String property, final String defaultValue ) {
65
    return getSettings().getString( property, defaultValue );
66
  }
67
68
  /**
69
   * Returns the value of a string property.
70
   *
71
   * @param property     The property key.
72
   * @param defaultValue The value to return if no property key has been set.
73
   * @return The property key value, or defaultValue when no key found.
74
   */
75
  @Override
76
  public int getSetting( final String property, final int defaultValue ) {
77
    return getSettings().getInt( property, defaultValue );
78
  }
79
80
  /**
81
   * Convert the generic list of property objects into strings.
82
   *
83
   * @param property The property value to coerce.
84
   * @param defaults The defaults values to use should the property be unset.
85
   * @return The list of properties coerced from objects to strings.
86
   */
87
  @Override
88
  public List<String> getStringSettingList(
89
      final String property, final List<String> defaults ) {
90
    return getSettings().getList( String.class, property, defaults );
91
  }
92
93
  /**
94
   * Convert a list of property objects into strings, with no default value.
95
   *
96
   * @param property The property value to coerce.
97
   * @return The list of properties coerced from objects to strings.
98
   */
99
  @Override
100
  public List<String> getStringSettingList( final String property ) {
101
    return getStringSettingList( property, null );
102
  }
103
104
  /**
105
   * Returns a list of property names that begin with the given prefix.
106
   *
107
   * @param prefix The prefix to compare against each property name.
108
   * @return The list of property names that have the given prefix.
109
   */
110
  @Override
111
  public Iterator<String> getKeys( final String prefix ) {
112
    return getSettings().getKeys( prefix );
113
  }
114
115
  private PropertiesConfiguration createProperties() {
116
    final var url = getPropertySource();
117
    final var configuration = new PropertiesConfiguration();
118
119
    if( url != null ) {
120
      try( final var reader = new InputStreamReader(
121
          url.openStream(), getDefaultEncoding() ) ) {
122
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
123
        configuration.read( reader );
124
      } catch( final Exception ex ) {
125
        throw new RuntimeException( ex );
126
      }
127
    }
128
129
    return configuration;
130
  }
131
132
  protected Charset getDefaultEncoding() {
133
    return Charset.defaultCharset();
134
  }
135
136
  protected ListDelimiterHandler createListDelimiterHandler() {
137
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
138
  }
139
140
  private URL getPropertySource() {
141
    return DefaultSettings.class.getResource( PATH_PROPERTIES_SETTINGS );
142
  }
143
144
  private void setProperties( final PropertiesConfiguration properties ) {
145
    mProperties = properties;
146
  }
147
148
  private PropertiesConfiguration getSettings() {
149
    return mProperties;
150
  }
151
}
1152
A src/main/java/com/keenwrite/service/impl/DefaultSnitch.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.service.impl;
29
30
import com.keenwrite.service.Snitch;
31
32
import java.io.IOException;
33
import java.nio.file.*;
34
import java.util.Collections;
35
import java.util.Map;
36
import java.util.Observable;
37
import java.util.Set;
38
import java.util.concurrent.ConcurrentHashMap;
39
40
import static com.keenwrite.Constants.APP_WATCHDOG_TIMEOUT;
41
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
42
43
/**
44
 * Listens for file changes. Other classes can register paths to be monitored
45
 * and listen for changes to those paths.
46
 */
47
public class DefaultSnitch extends Observable implements Snitch {
48
49
  /**
50
   * Service for listening to directories for modifications.
51
   */
52
  private WatchService watchService;
53
54
  /**
55
   * Directories being monitored for changes.
56
   */
57
  private Map<WatchKey, Path> keys;
58
59
  /**
60
   * Files that will kick off notification events if modified.
61
   */
62
  private Set<Path> eavesdropped;
63
64
  /**
65
   * Set to true when running; set to false to stop listening.
66
   */
67
  private volatile boolean listening;
68
69
  public DefaultSnitch() {
70
  }
71
72
  @Override
73
  public void stop() {
74
    setListening( false );
75
  }
76
77
  /**
78
   * Adds a listener to the list of files to watch for changes. If the file is
79
   * already in the monitored list, this will return immediately.
80
   *
81
   * @param file Path to a file to watch for changes.
82
   * @throws IOException The file could not be monitored.
83
   */
84
  @Override
85
  public void listen( final Path file ) throws IOException {
86
    if( file != null && getEavesdropped().add( file ) ) {
87
      final Path dir = toDirectory( file );
88
      final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
89
90
      getWatchMap().put( key, dir );
91
    }
92
  }
93
94
  /**
95
   * Returns the given path to a file (or directory) as a directory. If the
96
   * given path is already a directory, it is returned. Otherwise, this returns
97
   * the directory that contains the file. This will fail if the file is stored
98
   * in the root folder.
99
   *
100
   * @param path The file to return as a directory, which should always be the
101
   *             case.
102
   * @return The given path as a directory, if a file, otherwise the path
103
   * itself.
104
   */
105
  private Path toDirectory( final Path path ) {
106
    return Files.isDirectory( path )
107
        ? path
108
        : path.toFile().getParentFile().toPath();
109
  }
110
111
  /**
112
   * Stop listening to the given file for change events. This fails silently.
113
   *
114
   * @param file The file to no longer monitor for changes.
115
   */
116
  @Override
117
  public void ignore( final Path file ) {
118
    if( file != null ) {
119
      final Path directory = toDirectory( file );
120
121
      // Remove all occurrences (there should be only one).
122
      getWatchMap().values().removeAll( Collections.singleton( directory ) );
123
124
      // Remove all occurrences (there can be only one).
125
      getEavesdropped().remove( file );
126
    }
127
  }
128
129
  /**
130
   * Loops until stop is called, or the application is terminated.
131
   */
132
  @Override
133
  @SuppressWarnings("BusyWait")
134
  public void run() {
135
    setListening( true );
136
137
    while( isListening() ) {
138
      try {
139
        final WatchKey key = getWatchService().take();
140
        final Path path = get( key );
141
142
        // Prevent receiving two separate ENTRY_MODIFY events: file modified
143
        // and timestamp updated. Instead, receive one ENTRY_MODIFY event
144
        // with two counts.
145
        Thread.sleep( APP_WATCHDOG_TIMEOUT );
146
147
        for( final WatchEvent<?> event : key.pollEvents() ) {
148
          final Path changed = path.resolve( (Path) event.context() );
149
150
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
151
            setChanged();
152
            notifyObservers( changed );
153
          }
154
        }
155
156
        if( !key.reset() ) {
157
          ignore( path );
158
        }
159
      } catch( final IOException | InterruptedException ex ) {
160
        // Stop eavesdropping.
161
        setListening( false );
162
      }
163
    }
164
  }
165
166
  /**
167
   * Returns true if the list of files being listened to for changes contains
168
   * the given file.
169
   *
170
   * @param file Path to a system file.
171
   * @return true The given file is being monitored for changes.
172
   */
173
  private boolean isListening( final Path file ) {
174
    return getEavesdropped().contains( file );
175
  }
176
177
  /**
178
   * Returns a path for a given watch key.
179
   *
180
   * @param key The key to lookup its corresponding path.
181
   * @return The path for the given key.
182
   */
183
  private Path get( final WatchKey key ) {
184
    return getWatchMap().get( key );
185
  }
186
187
  private synchronized Map<WatchKey, Path> getWatchMap() {
188
    if( this.keys == null ) {
189
      this.keys = createWatchKeys();
190
    }
191
192
    return this.keys;
193
  }
194
195
  protected Map<WatchKey, Path> createWatchKeys() {
196
    return new ConcurrentHashMap<>();
197
  }
198
199
  /**
200
   * Returns a list of files that, when changed, will kick off a notification.
201
   *
202
   * @return A non-null, possibly empty, list of files.
203
   */
204
  private synchronized Set<Path> getEavesdropped() {
205
    if( this.eavesdropped == null ) {
206
      this.eavesdropped = createEavesdropped();
207
    }
208
209
    return this.eavesdropped;
210
  }
211
212
  protected Set<Path> createEavesdropped() {
213
    return ConcurrentHashMap.newKeySet();
214
  }
215
216
  /**
217
   * The existing watch service, or a new instance if null.
218
   *
219
   * @return A valid WatchService instance, never null.
220
   * @throws IOException Could not create a new watch service.
221
   */
222
  private synchronized WatchService getWatchService() throws IOException {
223
    if( this.watchService == null ) {
224
      this.watchService = createWatchService();
225
    }
226
227
    return this.watchService;
228
  }
229
230
  protected WatchService createWatchService() throws IOException {
231
    final FileSystem fileSystem = FileSystems.getDefault();
232
    return fileSystem.newWatchService();
233
  }
234
235
  /**
236
   * Answers whether the loop should continue executing.
237
   *
238
   * @return true The internal listening loop should continue listening for file
239
   * modification events.
240
   */
241
  protected boolean isListening() {
242
    return this.listening;
243
  }
244
245
  /**
246
   * Requests the snitch to stop eavesdropping on file changes.
247
   *
248
   * @param listening Use true to indicate the service should stop running.
249
   */
250
  private void setListening( final boolean listening ) {
251
    this.listening = listening;
252
  }
253
}
1254
A src/main/java/com/keenwrite/sigils/RSigilOperator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.sigils;
29
30
import static com.keenwrite.sigils.YamlSigilOperator.KEY_SEPARATOR_DEF;
31
32
/**
33
 * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils.
34
 */
35
public class RSigilOperator extends SigilOperator {
36
  public static final char KEY_SEPARATOR_R = '$';
37
38
  public static final String PREFIX = "`r#";
39
  public static final char SUFFIX = '`';
40
41
  private final String mDelimiterBegan =
42
      getUserPreferences().getRDelimiterBegan();
43
  private final String mDelimiterEnded =
44
      getUserPreferences().getRDelimiterEnded();
45
46
  /**
47
   * Returns the given string R-escaping backticks prepended and appended. This
48
   * is not null safe. Do not pass null into this method.
49
   *
50
   * @param key The string to adorn with R token delimiters.
51
   * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`".
52
   */
53
  @Override
54
  public String apply( final String key ) {
55
    assert key != null;
56
57
    return PREFIX
58
        + mDelimiterBegan
59
        + entoken( key )
60
        + mDelimiterEnded
61
        + SUFFIX;
62
  }
63
64
  /**
65
   * Transforms a definition key (bracketed by token delimiters) into the
66
   * expected format for an R variable key name.
67
   *
68
   * @param key The variable name to transform, can be empty but not null.
69
   * @return The transformed variable name.
70
   */
71
  public static String entoken( final String key ) {
72
    return "v$" +
73
        YamlSigilOperator.detoken( key )
74
                         .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R );
75
  }
76
}
177
A src/main/java/com/keenwrite/sigils/SigilOperator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.sigils;
29
30
import com.keenwrite.preferences.UserPreferences;
31
32
import java.util.function.UnaryOperator;
33
34
/**
35
 * Responsible for updating definition keys to use a machine-readable format
36
 * corresponding to the type of file being edited. This changes a definition
37
 * key name based on some criteria determined by the factory that creates
38
 * implementations of this interface.
39
 */
40
public abstract class SigilOperator implements UnaryOperator<String> {
41
  protected static UserPreferences getUserPreferences() {
42
    return UserPreferences.getInstance();
43
  }
44
}
145
A src/main/java/com/keenwrite/sigils/YamlSigilOperator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.sigils;
29
30
import java.util.regex.Pattern;
31
32
import static java.lang.String.format;
33
import static java.util.regex.Pattern.compile;
34
import static java.util.regex.Pattern.quote;
35
36
/**
37
 * Brackets definition keys with token delimiters.
38
 */
39
public class YamlSigilOperator extends SigilOperator {
40
  public static final char KEY_SEPARATOR_DEF = '.';
41
42
  private static final String mDelimiterBegan =
43
      getUserPreferences().getDefDelimiterBegan();
44
  private static final String mDelimiterEnded =
45
      getUserPreferences().getDefDelimiterEnded();
46
47
  /**
48
   * Non-greedy match of key names delimited by definition tokens.
49
   */
50
  private static final String REGEX =
51
      format( "(%s.*?%s)", quote( mDelimiterBegan ), quote( mDelimiterEnded ) );
52
53
  /**
54
   * Compiled regular expression for matching delimited references.
55
   */
56
  public static final Pattern REGEX_PATTERN = compile( REGEX );
57
58
  /**
59
   * Returns the given {@link String} verbatim because variables in YAML
60
   * documents and plain Markdown documents already have the appropriate
61
   * tokenizable syntax wrapped around the text.
62
   *
63
   * @param key Returned verbatim.
64
   */
65
  @Override
66
  public String apply( final String key ) {
67
    return key;
68
  }
69
70
  /**
71
   * Adds delimiters to the given key.
72
   *
73
   * @param key The key to adorn with start and stop definition tokens.
74
   * @return The given key bracketed by definition token symbols.
75
   */
76
  public static String entoken( final String key ) {
77
    assert key != null;
78
    return mDelimiterBegan + key + mDelimiterEnded;
79
  }
80
81
  /**
82
   * Removes start and stop definition key delimiters from the given key. This
83
   * method does not check for delimiters, only that there are sufficient
84
   * characters to remove from either end of the given key.
85
   *
86
   * @param key The key adorned with start and stop definition tokens.
87
   * @return The given key with the delimiters removed.
88
   */
89
  public static String detoken( final String key ) {
90
    final int beganLen = mDelimiterBegan.length();
91
    final int endedLen = mDelimiterEnded.length();
92
93
    return key.length() > beganLen + endedLen
94
        ? key.substring( beganLen, key.length() - endedLen )
95
        : key;
96
  }
97
}
198
A src/main/java/com/keenwrite/spelling/api/SpellCheckListener.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.spelling.api;
29
30
import java.util.function.BiConsumer;
31
32
/**
33
 * Represents an operation that accepts two input arguments and returns no
34
 * result. Unlike most other functional interfaces, this class is expected to
35
 * operate via side-effects.
36
 * <p>
37
 * This is used instead of a {@link BiConsumer} to avoid autoboxing.
38
 * </p>
39
 */
40
@FunctionalInterface
41
public interface SpellCheckListener {
42
43
  /**
44
   * Performs an operation on the given arguments.
45
   *
46
   * @param text        The text associated with a beginning and ending offset.
47
   * @param beganOffset A starting offset, used as an index into a string.
48
   * @param endedOffset An ending offset, which should equal text.length() +
49
   *                    beganOffset.
50
   */
51
  void accept( String text, int beganOffset, int endedOffset );
52
}
153
A src/main/java/com/keenwrite/spelling/api/SpellChecker.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.spelling.api;
29
30
import java.util.List;
31
32
/**
33
 * Defines the responsibilities for a spell checking API. The intention is
34
 * to allow different spell checking implementations to be used by the
35
 * application, such as SymSpell and LinSpell.
36
 */
37
public interface SpellChecker {
38
39
  /**
40
   * Answers whether the given lexeme, in whole, is found in the lexicon. The
41
   * lexicon lookup is performed case-insensitively. This method should be
42
   * used instead of {@link #suggestions(String, int)} for performance reasons.
43
   *
44
   * @param lexeme The word to check for correctness.
45
   * @return {@code true} if the lexeme is in the lexicon.
46
   */
47
  boolean inLexicon( String lexeme );
48
49
  /**
50
   * Gets a list of spelling corrections for the given lexeme.
51
   *
52
   * @param lexeme A word to check for correctness that's not in the lexicon.
53
   * @param count  The maximum number of alternatives to return.
54
   * @return A list of words in the lexicon that are similar to the given
55
   * lexeme.
56
   */
57
  List<String> suggestions( String lexeme, int count );
58
59
  /**
60
   * Iterates over the given text, emitting starting and ending offsets into
61
   * the text for every word that is missing from the lexicon.
62
   *
63
   * @param text     The text to check for words missing from the lexicon.
64
   * @param consumer Every missing word emits a message with the starting
65
   *                 and ending offset into the text where said word is found.
66
   */
67
  void proofread( String text, SpellCheckListener consumer );
68
}
169
A src/main/java/com/keenwrite/spelling/impl/PermissiveSpeller.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.spelling.impl;
29
30
import com.keenwrite.spelling.api.SpellCheckListener;
31
import com.keenwrite.spelling.api.SpellChecker;
32
33
import java.util.List;
34
35
/**
36
 * Responsible for spell checking in the event that a real spell checking
37
 * implementation cannot be created (for any reason). Does not perform any
38
 * spell checking and indicates that any given lexeme is in the lexicon.
39
 */
40
public class PermissiveSpeller implements SpellChecker {
41
  /**
42
   * Returns {@code true}, ignoring the given word.
43
   *
44
   * @param ignored Unused.
45
   * @return {@code true}
46
   */
47
  @Override
48
  public boolean inLexicon( final String ignored ) {
49
    return true;
50
  }
51
52
  /**
53
   * Returns an array with the given lexeme.
54
   *
55
   * @param lexeme  The word to return.
56
   * @param ignored Unused.
57
   * @return A suggestion list containing the given lexeme.
58
   */
59
  @Override
60
  public List<String> suggestions( final String lexeme, final int ignored ) {
61
    return List.of( lexeme );
62
  }
63
64
  /**
65
   * Performs no action.
66
   *
67
   * @param text    Unused.
68
   * @param ignored Uncalled.
69
   */
70
  @Override
71
  public void proofread(
72
      final String text, final SpellCheckListener ignored ) {
73
  }
74
}
175
A src/main/java/com/keenwrite/spelling/impl/SymSpellSpeller.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.spelling.impl;
29
30
import com.keenwrite.spelling.api.SpellCheckListener;
31
import com.keenwrite.spelling.api.SpellChecker;
32
import io.gitlab.rxp90.jsymspell.SuggestItem;
33
import io.gitlab.rxp90.jsymspell.SymSpell;
34
import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
35
36
import java.text.BreakIterator;
37
import java.util.ArrayList;
38
import java.util.Collection;
39
import java.util.List;
40
41
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
42
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
43
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
44
import static java.lang.Character.isLetter;
45
46
/**
47
 * Responsible for spell checking using {@link SymSpell}.
48
 */
49
public class SymSpellSpeller implements SpellChecker {
50
  private final BreakIterator mBreakIterator = BreakIterator.getWordInstance();
51
52
  private final SymSpell mSymSpell;
53
54
  /**
55
   * Creates a new lexicon for the given collection of lexemes.
56
   *
57
   * @param lexiconWords The words in the lexicon to add for spell checking,
58
   *                     must not be empty.
59
   * @return An instance of {@link SpellChecker} that can check if a word
60
   * is correct and suggest alternatives.
61
   */
62
  public static SpellChecker forLexicon(
63
      final Collection<String> lexiconWords ) {
64
    assert lexiconWords != null && !lexiconWords.isEmpty();
65
66
    final SymSpellBuilder builder = new SymSpellBuilder()
67
        .setLexiconWords( lexiconWords );
68
69
    return new SymSpellSpeller( builder.build() );
70
  }
71
72
  /**
73
   * Prevent direct instantiation so that only the {@link SpellChecker}
74
   * interface
75
   * is available.
76
   *
77
   * @param symSpell The implementation-specific spell checker.
78
   */
79
  private SymSpellSpeller( final SymSpell symSpell ) {
80
    mSymSpell = symSpell;
81
  }
82
83
  @Override
84
  public boolean inLexicon( final String lexeme ) {
85
    return lookup( lexeme, CLOSEST ).size() == 1;
86
  }
87
88
  @Override
89
  public List<String> suggestions( final String lexeme, int count ) {
90
    final List<String> result = new ArrayList<>( count );
91
92
    for( final var item : lookup( lexeme, ALL ) ) {
93
      if( count-- > 0 ) {
94
        result.add( item.getSuggestion() );
95
      }
96
      else {
97
        break;
98
      }
99
    }
100
101
    return result;
102
  }
103
104
  @Override
105
  public void proofread(
106
      final String text, final SpellCheckListener consumer ) {
107
    assert text != null;
108
    assert consumer != null;
109
110
    mBreakIterator.setText( text );
111
112
    int boundaryIndex = mBreakIterator.first();
113
    int previousIndex = 0;
114
115
    while( boundaryIndex != BreakIterator.DONE ) {
116
      final var lex = text.substring( previousIndex, boundaryIndex )
117
                          .toLowerCase();
118
119
      // Get the lexeme for the possessive.
120
      final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" );
121
      final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex;
122
123
      if( isWord( lexeme ) && !inLexicon( lexeme ) ) {
124
        consumer.accept( lex, previousIndex, boundaryIndex );
125
      }
126
127
      previousIndex = boundaryIndex;
128
      boundaryIndex = mBreakIterator.next();
129
    }
130
  }
131
132
  /**
133
   * Answers whether the given string is likely a word by checking the first
134
   * character.
135
   *
136
   * @param word The word to check.
137
   * @return {@code true} if the word begins with a letter.
138
   */
139
  private boolean isWord( final String word ) {
140
    return !word.isEmpty() && isLetter( word.charAt( 0 ) );
141
  }
142
143
  /**
144
   * Returns a list of {@link SuggestItem} instances that provide alternative
145
   * spellings for the given lexeme.
146
   *
147
   * @param lexeme A word to look up in the lexicon.
148
   * @param v      Influences the number of results returned.
149
   * @return Alternative lexemes.
150
   */
151
  private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
152
    return getSpeller().lookup( lexeme, v );
153
  }
154
155
  private SymSpell getSpeller() {
156
    return mSymSpell;
157
  }
158
}
1159
A src/main/java/com/keenwrite/util/Action.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.util;
28
29
import de.jensd.fx.glyphs.GlyphIcons;
30
import javafx.beans.value.ObservableBooleanValue;
31
import javafx.event.ActionEvent;
32
import javafx.event.EventHandler;
33
import javafx.scene.input.KeyCombination;
34
35
/**
36
 * Defines actions the user can take by interacting with the GUI.
37
 */
38
public class Action {
39
  public final String text;
40
  public final KeyCombination accelerator;
41
  public final GlyphIcons icon;
42
  public final EventHandler<ActionEvent> action;
43
  public final ObservableBooleanValue disable;
44
45
  public Action(
46
      final String text,
47
      final String accelerator,
48
      final GlyphIcons icon,
49
      final EventHandler<ActionEvent> action,
50
      final ObservableBooleanValue disable ) {
51
52
    this.text = text;
53
    this.accelerator = accelerator == null ?
54
        null : KeyCombination.valueOf( accelerator );
55
    this.icon = icon;
56
    this.action = action;
57
    this.disable = disable;
58
  }
59
}
160
A src/main/java/com/keenwrite/util/ActionBuilder.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.util;
29
30
import com.keenwrite.Messages;
31
import de.jensd.fx.glyphs.GlyphIcons;
32
import javafx.beans.value.ObservableBooleanValue;
33
import javafx.event.ActionEvent;
34
import javafx.event.EventHandler;
35
36
/**
37
 * Provides a fluent interface around constructing actions so that duplication
38
 * can be avoided.
39
 */
40
public class ActionBuilder {
41
  private String mText;
42
  private String mAccelerator;
43
  private GlyphIcons mIcon;
44
  private EventHandler<ActionEvent> mAction;
45
  private ObservableBooleanValue mDisable;
46
47
  /**
48
   * Sets the action text based on a resource bundle key.
49
   *
50
   * @param key The key to look up in the {@link Messages}.
51
   * @return The corresponding value, or the key name if none found.
52
   */
53
  public ActionBuilder setText( final String key ) {
54
    mText = Messages.get( key, key );
55
    return this;
56
  }
57
58
  public ActionBuilder setAccelerator( final String accelerator ) {
59
    mAccelerator = accelerator;
60
    return this;
61
  }
62
63
  public ActionBuilder setIcon( final GlyphIcons icon ) {
64
    mIcon = icon;
65
    return this;
66
  }
67
68
  public ActionBuilder setAction( final EventHandler<ActionEvent> action ) {
69
    mAction = action;
70
    return this;
71
  }
72
73
  public ActionBuilder setDisable( final ObservableBooleanValue disable ) {
74
    mDisable = disable;
75
    return this;
76
  }
77
78
  public Action build() {
79
    return new Action( mText, mAccelerator, mIcon, mAction, mDisable );
80
  }
81
}
182
A src/main/java/com/keenwrite/util/ActionUtils.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.util;
28
29
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
30
import javafx.scene.Node;
31
import javafx.scene.control.Button;
32
import javafx.scene.control.Menu;
33
import javafx.scene.control.MenuItem;
34
import javafx.scene.control.Separator;
35
import javafx.scene.control.SeparatorMenuItem;
36
import javafx.scene.control.ToolBar;
37
import javafx.scene.control.Tooltip;
38
39
/**
40
 * Responsible for creating menu items and toolbar buttons.
41
 */
42
public class ActionUtils {
43
44
  public static Menu createMenu( final String text, final Action... actions ) {
45
    return new Menu( text, null, createMenuItems( actions ) );
46
  }
47
48
  public static MenuItem[] createMenuItems( final Action... actions ) {
49
    final MenuItem[] menuItems = new MenuItem[ actions.length ];
50
51
    for( int i = 0; i < actions.length; i++ ) {
52
      menuItems[ i ] = (actions[ i ] == null)
53
          ? new SeparatorMenuItem()
54
          : createMenuItem( actions[ i ] );
55
    }
56
57
    return menuItems;
58
  }
59
60
  public static MenuItem createMenuItem( final Action action ) {
61
    final MenuItem menuItem = new MenuItem( action.text );
62
63
    if( action.accelerator != null ) {
64
      menuItem.setAccelerator( action.accelerator );
65
    }
66
67
    if( action.icon != null ) {
68
      menuItem.setGraphic(
69
          FontAwesomeIconFactory.get().createIcon( action.icon ) );
70
    }
71
72
    menuItem.setOnAction( action.action );
73
74
    if( action.disable != null ) {
75
      menuItem.disableProperty().bind( action.disable );
76
    }
77
78
    menuItem.setMnemonicParsing( true );
79
80
    return menuItem;
81
  }
82
83
  public static ToolBar createToolBar( final Action... actions ) {
84
    return new ToolBar( createToolBarButtons( actions ) );
85
  }
86
87
  public static Node[] createToolBarButtons( final Action... actions ) {
88
    Node[] buttons = new Node[ actions.length ];
89
    for( int i = 0; i < actions.length; i++ ) {
90
      buttons[ i ] = (actions[ i ] != null)
91
          ? createToolBarButton( actions[ i ] )
92
          : new Separator();
93
    }
94
    return buttons;
95
  }
96
97
  public static Button createToolBarButton( final Action action ) {
98
    final Button button = new Button();
99
    button.setGraphic(
100
        FontAwesomeIconFactory
101
            .get()
102
            .createIcon( action.icon, "1.2em" ) );
103
104
    String tooltip = action.text;
105
106
    if( tooltip.endsWith( "..." ) ) {
107
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
108
    }
109
110
    if( action.accelerator != null ) {
111
      tooltip += " (" + action.accelerator.getDisplayText() + ')';
112
    }
113
114
    button.setTooltip( new Tooltip( tooltip ) );
115
    button.setFocusTraversable( false );
116
    button.setOnAction( action.action );
117
118
    if( action.disable != null ) {
119
      button.disableProperty().bind( action.disable );
120
    }
121
122
    return button;
123
  }
124
}
1125
A src/main/java/com/keenwrite/util/BoundedCache.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.util;
29
30
import java.util.LinkedHashMap;
31
import java.util.Map;
32
33
/**
34
 * A map that removes the oldest entry once its capacity (cache size) has
35
 * been reached.
36
 *
37
 * @param <K> The type of key mapped to a value.
38
 * @param <V> The type of value mapped to a key.
39
 */
40
public class BoundedCache<K, V> extends LinkedHashMap<K, V> {
41
  private final int mCacheSize;
42
43
  /**
44
   * Constructs a new instance having a finite size.
45
   *
46
   * @param cacheSize The maximum number of entries.
47
   */
48
  public BoundedCache( final int cacheSize ) {
49
    mCacheSize = cacheSize;
50
  }
51
52
  @Override
53
  protected boolean removeEldestEntry(
54
      final Map.Entry<K, V> eldest ) {
55
    return size() > mCacheSize;
56
  }
57
}
158
A src/main/java/com/keenwrite/util/ProtocolResolver.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.util;
29
30
import java.io.File;
31
import java.net.MalformedURLException;
32
import java.net.URI;
33
import java.net.URL;
34
35
import static com.keenwrite.util.ProtocolScheme.UNKNOWN;
36
37
/**
38
 * Responsible for determining the protocol of a resource.
39
 */
40
public class ProtocolResolver {
41
  /**
42
   * Returns the protocol for a given URI or filename.
43
   *
44
   * @param resource Determine the protocol for this URI or filename.
45
   * @return The protocol for the given resource.
46
   */
47
  public static ProtocolScheme getProtocol( final String resource ) {
48
    String protocol;
49
50
    try {
51
      final URI uri = new URI( resource );
52
53
      if( uri.isAbsolute() ) {
54
        protocol = uri.getScheme();
55
      }
56
      else {
57
        final URL url = new URL( resource );
58
        protocol = url.getProtocol();
59
      }
60
    } catch( final Exception e ) {
61
      // Could be HTTP, HTTPS?
62
      if( resource.startsWith( "//" ) ) {
63
        throw new IllegalArgumentException( "Relative context: " + resource );
64
      }
65
      else {
66
        final File file = new File( resource );
67
        protocol = getProtocol( file );
68
      }
69
    }
70
71
    return ProtocolScheme.valueFrom( protocol );
72
  }
73
74
  /**
75
   * Returns the protocol for a given file.
76
   *
77
   * @param file Determine the protocol for this file.
78
   * @return The protocol for the given file.
79
   */
80
  private static String getProtocol( final File file ) {
81
    String result;
82
83
    try {
84
      result = file.toURI().toURL().getProtocol();
85
    } catch( final MalformedURLException ex ) {
86
      // Value guaranteed to avoid identification as a standard protocol.
87
      result = UNKNOWN.toString();
88
    }
89
90
    return result;
91
  }
92
}
193
A src/main/java/com/keenwrite/util/ProtocolScheme.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.util;
29
30
/**
31
 * Represents the type of data encoding scheme used for a universal resource
32
 * indicator.
33
 */
34
public enum ProtocolScheme {
35
  /**
36
   * Denotes either HTTP or HTTPS.
37
   */
38
  HTTP,
39
  /**
40
   * Denotes a local file.
41
   */
42
  FILE,
43
  /**
44
   * Could not determine schema (or is not supported by the application).
45
   */
46
  UNKNOWN;
47
48
  /**
49
   * Answers {@code true} if the given protocol is either HTTP or HTTPS.
50
   *
51
   * @return {@code true} the protocol is either HTTP or HTTPS.
52
   */
53
  public boolean isHttp() {
54
    return this == HTTP;
55
  }
56
57
  /**
58
   * Answers {@code true} if the given protocol is for a local file.
59
   *
60
   * @return {@code true} the protocol is for a local file reference.
61
   */
62
  public boolean isFile() {
63
    return this == FILE;
64
  }
65
66
  /**
67
   * Determines the protocol scheme for a given string.
68
   *
69
   * @param protocol A string representing data encoding protocol scheme.
70
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
71
   * valid value from this enumeration.
72
   */
73
  public static ProtocolScheme valueFrom( String protocol ) {
74
    ProtocolScheme result = UNKNOWN;
75
    protocol = sanitize( protocol );
76
77
    for( final var scheme : values() ) {
78
      // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate.
79
      if( protocol.startsWith( scheme.name() ) ) {
80
        result = scheme;
81
        break;
82
      }
83
    }
84
85
    return result;
86
  }
87
88
  /**
89
   * Returns an empty string if the given string to sanitize is {@code null},
90
   * otherwise the given string in uppercase. Uppercase is used to align with
91
   * the enum name.
92
   *
93
   * @param s The string to sanitize, may be {@code null}.
94
   * @return A non-{@code null} string.
95
   */
96
  private static String sanitize( final String s ) {
97
    return s == null ? "" : s.toUpperCase();
98
  }
99
}
1100
A src/main/java/com/keenwrite/util/ResourceWalker.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.util;
29
30
import java.io.IOException;
31
import java.net.URISyntaxException;
32
import java.nio.file.*;
33
import java.util.function.Consumer;
34
35
import static java.nio.file.FileSystems.newFileSystem;
36
import static java.util.Collections.emptyMap;
37
38
/**
39
 * Responsible for finding file resources.
40
 */
41
public class ResourceWalker {
42
  private static final PathMatcher PATH_MATCHER =
43
      FileSystems.getDefault().getPathMatcher( "glob:**.{ttf,otf}" );
44
45
  /**
46
   * @param dirName The root directory to scan for files matching the glob.
47
   * @param c       The consumer function to call for each matching path found.
48
   * @throws URISyntaxException Could not convert the resource to a URI.
49
   * @throws IOException        Could not walk the tree.
50
   */
51
  public static void walk( final String dirName, final Consumer<Path> c )
52
      throws URISyntaxException, IOException {
53
    final var resource = ResourceWalker.class.getResource( dirName );
54
55
    if( resource != null ) {
56
      final var uri = resource.toURI();
57
      final var path = uri.getScheme().equals( "jar" )
58
          ? newFileSystem( uri, emptyMap() ).getPath( dirName )
59
          : Paths.get( uri );
60
      final var walk = Files.walk( path, 10 );
61
62
      for( final var it = walk.iterator(); it.hasNext(); ) {
63
        final Path p = it.next();
64
        if( PATH_MATCHER.matches( p ) ) {
65
          c.accept( p );
66
        }
67
      }
68
    }
69
  }
70
}
171
A src/main/java/com/keenwrite/util/StageState.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.util;
28
29
import java.util.prefs.Preferences;
30
31
import javafx.application.Platform;
32
import javafx.scene.shape.Rectangle;
33
import javafx.stage.Stage;
34
import javafx.stage.WindowEvent;
35
36
/**
37
 * Saves and restores Stage state (window bounds, maximized, fullScreen).
38
 */
39
public class StageState {
40
41
  public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
42
  public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
43
  public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
44
45
  private final Stage mStage;
46
  private final Preferences mState;
47
48
  private Rectangle normalBounds;
49
  private boolean runLaterPending;
50
51
  public StageState( final Stage stage, final Preferences state ) {
52
    mStage = stage;
53
    mState = state;
54
55
    restore();
56
57
    stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
58
59
    stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() );
60
    stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() );
61
    stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() );
62
    stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() );
63
  }
64
65
  private void save() {
66
    final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
67
68
    if( bounds != null ) {
69
      mState.putDouble( "windowX", bounds.getX() );
70
      mState.putDouble( "windowY", bounds.getY() );
71
      mState.putDouble( "windowWidth", bounds.getWidth() );
72
      mState.putDouble( "windowHeight", bounds.getHeight() );
73
    }
74
75
    mState.putBoolean( "windowMaximized", mStage.isMaximized() );
76
    mState.putBoolean( "windowFullScreen", mStage.isFullScreen() );
77
  }
78
79
  private void restore() {
80
    final double x = mState.getDouble( "windowX", Double.NaN );
81
    final double y = mState.getDouble( "windowY", Double.NaN );
82
    final double w = mState.getDouble( "windowWidth", Double.NaN );
83
    final double h = mState.getDouble( "windowHeight", Double.NaN );
84
    final boolean maximized = mState.getBoolean( "windowMaximized", false );
85
    final boolean fullScreen = mState.getBoolean( "windowFullScreen", false );
86
87
    if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
88
      mStage.setX( x );
89
      mStage.setY( y );
90
    } // else: default behavior is center on screen
91
92
    if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
93
      mStage.setWidth( w );
94
      mStage.setHeight( h );
95
    } // else: default behavior is use scene size
96
97
    if( fullScreen != mStage.isFullScreen() ) {
98
      mStage.setFullScreen( fullScreen );
99
    }
100
101
    if( maximized != mStage.isMaximized() ) {
102
      mStage.setMaximized( maximized );
103
    }
104
  }
105
106
  /**
107
   * Remembers the window bounds when the window is not iconified, maximized or
108
   * in fullScreen.
109
   */
110
  private void boundsChanged() {
111
    // avoid too many (and useless) runLater() invocations
112
    if( runLaterPending ) {
113
      return;
114
    }
115
116
    runLaterPending = true;
117
118
    // must use runLater() to ensure that change of all properties
119
    // (x, y, width, height, iconified, maximized and fullScreen)
120
    // has finished
121
    Platform.runLater( () -> {
122
      runLaterPending = false;
123
124
      if( isNormalState() ) {
125
        normalBounds = getStageBounds();
126
      }
127
    } );
128
  }
129
130
  private boolean isNormalState() {
131
    return !mStage.isIconified() &&
132
        !mStage.isMaximized() &&
133
        !mStage.isFullScreen();
134
  }
135
136
  private Rectangle getStageBounds() {
137
    return new Rectangle(
138
        mStage.getX(),
139
        mStage.getY(),
140
        mStage.getWidth(),
141
        mStage.getHeight()
142
    );
143
  }
144
}
1145
A src/main/java/com/keenwrite/util/Utils.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.keenwrite.util;
28
29
import java.util.ArrayList;
30
import java.util.prefs.Preferences;
31
32
/**
33
 * Responsible for trimming, storing, and retrieving strings.
34
 */
35
public class Utils {
36
37
  public static String ltrim( final String s ) {
38
    int i = 0;
39
40
    while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) {
41
      i++;
42
    }
43
44
    return s.substring( i );
45
  }
46
47
  public static String rtrim( final String s ) {
48
    int i = s.length() - 1;
49
50
    while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) {
51
      i--;
52
    }
53
54
    return s.substring( 0, i + 1 );
55
  }
56
57
  public static String[] getPrefsStrings( final Preferences prefs,
58
                                          String key ) {
59
    final ArrayList<String> arr = new ArrayList<>( 256 );
60
61
    for( int i = 0; i < 10000; i++ ) {
62
      final String s = prefs.get( key + (i + 1), null );
63
64
      if( s == null ) {
65
        break;
66
      }
67
68
      arr.add( s );
69
    }
70
71
    return arr.toArray( new String[ 0 ] );
72
  }
73
74
  public static void putPrefsStrings( Preferences prefs, String key,
75
                                      String[] strings ) {
76
    for( int i = 0; i < strings.length; i++ ) {
77
      prefs.put( key + (i + 1), strings[ i ] );
78
    }
79
80
    for( int i = strings.length; prefs.get( key + (i + 1),
81
                                            null ) != null; i++ ) {
82
      prefs.remove( key + (i + 1) );
83
    }
84
  }
85
}
186
D src/main/java/com/scrivenvar/AbstractFileFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.Settings;
31
import com.scrivenvar.util.ProtocolScheme;
32
33
import java.nio.file.Path;
34
35
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
36
import static com.scrivenvar.Constants.SETTINGS;
37
import static com.scrivenvar.FileType.UNKNOWN;
38
import static com.scrivenvar.predicates.PredicateFactory.createFileTypePredicate;
39
import static java.lang.String.format;
40
41
/**
42
 * Provides common behaviours for factories that instantiate classes based on
43
 * file type.
44
 */
45
public class AbstractFileFactory {
46
47
  private static final String MSG_UNKNOWN_FILE_TYPE =
48
      "Unknown type '%s' for file '%s'.";
49
50
  /**
51
   * Determines the file type from the path extension. This should only be
52
   * called when it is known that the file type won't be a definition file
53
   * (e.g., YAML or other definition source), but rather an editable file
54
   * (e.g., Markdown, XML, etc.).
55
   *
56
   * @param path The path with a file name extension.
57
   * @return The FileType for the given path.
58
   */
59
  public FileType lookup( final Path path ) {
60
    return lookup( path, GLOB_PREFIX_FILE );
61
  }
62
63
  /**
64
   * Creates a file type that corresponds to the given path.
65
   *
66
   * @param path   Reference to a variable definition file.
67
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
68
   * @return The file type that corresponds to the given path.
69
   */
70
  protected FileType lookup( final Path path, final String prefix ) {
71
    assert path != null;
72
    assert prefix != null;
73
74
    final var settings = getSettings();
75
    final var keys = settings.getKeys( prefix );
76
77
    var found = false;
78
    var fileType = UNKNOWN;
79
80
    while( keys.hasNext() && !found ) {
81
      final var key = keys.next();
82
      final var patterns = settings.getStringSettingList( key );
83
      final var predicate = createFileTypePredicate( patterns );
84
85
      if( found = predicate.test( path.toFile() ) ) {
86
        // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
87
        // to a standard name (as defined in the settings.properties file).
88
        final String suffix = key.replace( prefix + ".", "" );
89
        fileType = FileType.from( suffix );
90
      }
91
    }
92
93
    return fileType;
94
  }
95
96
  /**
97
   * Throws IllegalArgumentException because the given path could not be
98
   * recognized. This exists because
99
   *
100
   * @param type The detected path type (protocol, file extension, etc.).
101
   * @param path The path to a source of definitions.
102
   */
103
  protected void unknownFileType(
104
      final ProtocolScheme type, final String path ) {
105
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
106
    throw new IllegalArgumentException( msg );
107
  }
108
109
  /**
110
   * Return the singleton Settings instance.
111
   *
112
   * @return A non-null instance.
113
   */
114
  private Settings getSettings() {
115
    return SETTINGS;
116
  }
117
}
1181
D src/main/java/com/scrivenvar/Constants.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.Settings;
31
32
import java.nio.file.Path;
33
import java.nio.file.Paths;
34
35
/**
36
 * Defines application-wide default values.
37
 */
38
public class Constants {
39
40
  public static final Settings SETTINGS = Services.load( Settings.class );
41
42
  /**
43
   * Prevent instantiation.
44
   */
45
  private Constants() {
46
  }
47
48
  private static String get( final String key ) {
49
    return SETTINGS.getSetting( key, "" );
50
  }
51
52
  @SuppressWarnings("SameParameterValue")
53
  private static int get( final String key, final int defaultValue ) {
54
    return SETTINGS.getSetting( key, defaultValue );
55
  }
56
57
  // Bootstrapping...
58
  public static final String SETTINGS_NAME =
59
      "/com/scrivenvar/settings.properties";
60
61
  public static final String DEFINITION_NAME = "variables.yaml";
62
63
  public static final String APP_TITLE = get( "application.title" );
64
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
65
66
  // Prevent double events when updating files on Linux (save and timestamp).
67
  public static final int APP_WATCHDOG_TIMEOUT = get(
68
      "application.watchdog.timeout", 200 );
69
70
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
71
  public static final String STYLESHEET_MARKDOWN = get(
72
      "file.stylesheet.markdown" );
73
  public static final String STYLESHEET_PREVIEW = get(
74
      "file.stylesheet.preview" );
75
76
  public static final String FILE_LOGO_16 = get( "file.logo.16" );
77
  public static final String FILE_LOGO_32 = get( "file.logo.32" );
78
  public static final String FILE_LOGO_128 = get( "file.logo.128" );
79
  public static final String FILE_LOGO_256 = get( "file.logo.256" );
80
  public static final String FILE_LOGO_512 = get( "file.logo.512" );
81
82
  public static final String PREFS_ROOT = get( "preferences.root" );
83
  public static final String PREFS_STATE = get( "preferences.root.state" );
84
85
  /**
86
   * Refer to filename extension settings in the configuration file. Do not
87
   * terminate these prefixes with a period.
88
   */
89
  public static final String GLOB_PREFIX_FILE = "file.ext";
90
  public static final String GLOB_PREFIX_DEFINITION =
91
      "definition." + GLOB_PREFIX_FILE;
92
93
  /**
94
   * Three parameters: line number, column number, and offset.
95
   */
96
  public static final String STATUS_BAR_LINE = "Main.status.line";
97
98
  public static final String STATUS_BAR_OK = "Main.status.state.default";
99
100
  /**
101
   * Used to show an error while parsing, usually syntactical.
102
   */
103
  public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
104
  public static final String STATUS_DEFINITION_BLANK = "Main.status.error.def.blank";
105
  public static final String STATUS_DEFINITION_EMPTY = "Main.status.error.def.empty";
106
107
  /**
108
   * One parameter: the word under the cursor that could not be found.
109
   */
110
  public static final String STATUS_DEFINITION_MISSING = "Main.status.error.def.missing";
111
112
  /**
113
   * Used when creating flat maps relating to resolved variables.
114
   */
115
  public static final int DEFAULT_MAP_SIZE = 64;
116
117
  /**
118
   * Default image extension order to use when scanning.
119
   */
120
  public static final String PERSIST_IMAGES_DEFAULT =
121
      get( "file.ext.image.order" );
122
123
  /**
124
   * Default working directory to use for R startup script.
125
   */
126
  public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
127
128
  /**
129
   * Default path to use for an untitled (pathless) file.
130
   */
131
  public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY );
132
133
  /**
134
   * Default starting delimiter for definition variables.
135
   */
136
  public static final String DEF_DELIM_BEGAN_DEFAULT = "${";
137
138
  /**
139
   * Default ending delimiter for definition variables.
140
   */
141
  public static final String DEF_DELIM_ENDED_DEFAULT = "}";
142
143
  /**
144
   * Default starting delimiter when inserting R variables.
145
   */
146
  public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
147
148
  /**
149
   * Default ending delimiter when inserting R variables.
150
   */
151
  public static final String R_DELIM_ENDED_DEFAULT = " )";
152
153
  /**
154
   * Resource directory where different language lexicons are located.
155
   */
156
  public static final String LEXICONS_DIRECTORY = "lexicons";
157
158
  /**
159
   * Used as the prefix for uniquely identifying HTML block elements, which
160
   * helps coordinate scrolling the preview pane to where the user is typing.
161
   */
162
  public static final String PARAGRAPH_ID_PREFIX = "p-";
163
164
  /**
165
   * Absolute location of true type font files within the Java archive file.
166
   */
167
  public static final String FONT_DIRECTORY = "/fonts";
168
169
  /**
170
   * Default text editor font size, in points.
171
   */
172
  public static final float FONT_SIZE_EDITOR = 12f;
173
}
1741
D src/main/java/com/scrivenvar/FileEditorTab.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * Redistribution and use in source and binary forms, with or without
5
 * modification, are permitted provided that the following conditions are met:
6
 *
7
 *  o Redistributions of source code must retain the above copyright
8
 *    notice, this list of conditions and the following disclaimer.
9
 *
10
 *  o Redistributions in binary form must reproduce the above copyright
11
 *    notice, this list of conditions and the following disclaimer in the
12
 *    documentation and/or other materials provided with the distribution.
13
 *
14
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
 */
26
package com.scrivenvar;
27
28
import com.scrivenvar.editors.EditorPane;
29
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
30
import com.scrivenvar.service.events.Notification;
31
import com.scrivenvar.service.events.Notifier;
32
import javafx.beans.binding.Bindings;
33
import javafx.beans.property.BooleanProperty;
34
import javafx.beans.property.ReadOnlyBooleanProperty;
35
import javafx.beans.property.ReadOnlyBooleanWrapper;
36
import javafx.beans.property.SimpleBooleanProperty;
37
import javafx.beans.value.ChangeListener;
38
import javafx.event.Event;
39
import javafx.event.EventHandler;
40
import javafx.event.EventType;
41
import javafx.scene.Scene;
42
import javafx.scene.control.Tab;
43
import javafx.scene.control.Tooltip;
44
import javafx.scene.text.Text;
45
import javafx.stage.Window;
46
import org.fxmisc.flowless.VirtualizedScrollPane;
47
import org.fxmisc.richtext.StyleClassedTextArea;
48
import org.fxmisc.undo.UndoManager;
49
import org.jetbrains.annotations.NotNull;
50
import org.mozilla.universalchardet.UniversalDetector;
51
52
import java.io.File;
53
import java.nio.charset.Charset;
54
import java.nio.file.Files;
55
import java.nio.file.Path;
56
57
import static com.scrivenvar.Messages.get;
58
import static com.scrivenvar.StatusBarNotifier.alert;
59
import static com.scrivenvar.StatusBarNotifier.getNotifier;
60
import static java.nio.charset.StandardCharsets.UTF_8;
61
import static java.util.Locale.ENGLISH;
62
import static javafx.application.Platform.runLater;
63
64
/**
65
 * Editor for a single file.
66
 */
67
public final class FileEditorTab extends Tab {
68
69
  private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane();
70
71
  private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
72
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
73
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
74
75
  /**
76
   * Character encoding used by the file (or default encoding if none found).
77
   */
78
  private Charset mEncoding = UTF_8;
79
80
  /**
81
   * File to load into the editor.
82
   */
83
  private Path mPath;
84
85
  public FileEditorTab( final Path path ) {
86
    setPath( path );
87
88
    mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
89
90
    setOnSelectionChanged( e -> {
91
      if( isSelected() ) {
92
        runLater( this::activated );
93
        requestFocus();
94
      }
95
    } );
96
  }
97
98
  private void updateTab() {
99
    setText( getTabTitle() );
100
    setGraphic( getModifiedMark() );
101
    setTooltip( getTabTooltip() );
102
  }
103
104
  /**
105
   * Returns the base filename (without the directory names).
106
   *
107
   * @return The untitled text if the path hasn't been set.
108
   */
109
  private String getTabTitle() {
110
    return getPath().getFileName().toString();
111
  }
112
113
  /**
114
   * Returns the full filename represented by the path.
115
   *
116
   * @return The untitled text if the path hasn't been set.
117
   */
118
  private Tooltip getTabTooltip() {
119
    final Path filePath = getPath();
120
    return new Tooltip( filePath == null ? "" : filePath.toString() );
121
  }
122
123
  /**
124
   * Returns a marker to indicate whether the file has been modified.
125
   *
126
   * @return "*" when the file has changed; otherwise null.
127
   */
128
  private Text getModifiedMark() {
129
    return isModified() ? new Text( "*" ) : null;
130
  }
131
132
  /**
133
   * Called when the user switches tab.
134
   */
135
  private void activated() {
136
    // Tab is closed or no longer active.
137
    if( getTabPane() == null || !isSelected() ) {
138
      return;
139
    }
140
141
    // If the tab is devoid of content, load it.
142
    if( getContent() == null ) {
143
      readFile();
144
      initLayout();
145
      initUndoManager();
146
    }
147
  }
148
149
  private void initLayout() {
150
    setContent( getScrollPane() );
151
  }
152
153
  /**
154
   * Tracks undo requests, but can only be called <em>after</em> load.
155
   */
156
  private void initUndoManager() {
157
    final UndoManager<?> undoManager = getUndoManager();
158
    undoManager.forgetHistory();
159
160
    // Bind the editor undo manager to the properties.
161
    mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
162
    canUndo.bind( undoManager.undoAvailableProperty() );
163
    canRedo.bind( undoManager.redoAvailableProperty() );
164
  }
165
166
  private void requestFocus() {
167
    getEditorPane().requestFocus();
168
  }
169
170
  /**
171
   * Searches from the caret position forward for the given string.
172
   *
173
   * @param needle The text string to match.
174
   */
175
  public void searchNext( final String needle ) {
176
    final String haystack = getEditorText();
177
    int index = haystack.indexOf( needle, getCaretPosition() );
178
179
    // Wrap around.
180
    if( index == -1 ) {
181
      index = haystack.indexOf( needle );
182
    }
183
184
    if( index >= 0 ) {
185
      setCaretPosition( index );
186
      getEditor().selectRange( index, index + needle.length() );
187
    }
188
  }
189
190
  /**
191
   * Gets a reference to the scroll pane that houses the editor.
192
   *
193
   * @return The editor's scroll pane, containing a vertical scrollbar.
194
   */
195
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
196
    return getEditorPane().getScrollPane();
197
  }
198
199
  /**
200
   * Returns the index into the text where the caret blinks happily away.
201
   *
202
   * @return A number from 0 to the editor's document text length.
203
   */
204
  public int getCaretPosition() {
205
    return getEditor().getCaretPosition();
206
  }
207
208
  /**
209
   * Moves the caret to a given offset.
210
   *
211
   * @param offset The new caret offset.
212
   */
213
  private void setCaretPosition( final int offset ) {
214
    getEditor().moveTo( offset );
215
    getEditor().requestFollowCaret();
216
  }
217
218
  /**
219
   * Returns the text area associated with this tab.
220
   *
221
   * @return A text editor.
222
   */
223
  private StyleClassedTextArea getEditor() {
224
    return getEditorPane().getEditor();
225
  }
226
227
  /**
228
   * Returns true if the given path exactly matches this tab's path.
229
   *
230
   * @param check The path to compare against.
231
   * @return true The paths are the same.
232
   */
233
  public boolean isPath( final Path check ) {
234
    final Path filePath = getPath();
235
236
    return filePath != null && filePath.equals( check );
237
  }
238
239
  /**
240
   * Reads the entire file contents from the path associated with this tab.
241
   */
242
  private void readFile() {
243
    final Path path = getPath();
244
    final File file = path.toFile();
245
246
    try {
247
      if( file.exists() ) {
248
        if( file.canWrite() && file.canRead() ) {
249
          final EditorPane pane = getEditorPane();
250
          pane.setText( asString( Files.readAllBytes( path ) ) );
251
          pane.scrollToTop();
252
        }
253
        else {
254
          final String msg = get( "FileEditor.loadFailed.reason.permissions" );
255
          alert( "FileEditor.loadFailed.message", file.toString(), msg );
256
        }
257
      }
258
    } catch( final Exception ex ) {
259
      alert( ex );
260
    }
261
  }
262
263
  /**
264
   * Saves the entire file contents from the path associated with this tab.
265
   *
266
   * @return true The file has been saved.
267
   */
268
  public boolean save() {
269
    try {
270
      final EditorPane editor = getEditorPane();
271
      Files.write( getPath(), asBytes( editor.getText() ) );
272
      editor.getUndoManager().mark();
273
      return true;
274
    } catch( final Exception ex ) {
275
      return popupAlert(
276
          "FileEditor.saveFailed.title",
277
          "FileEditor.saveFailed.message",
278
          ex
279
      );
280
    }
281
  }
282
283
  /**
284
   * Creates an alert dialog and waits for it to close.
285
   *
286
   * @param titleKey   Resource bundle key for the alert dialog title.
287
   * @param messageKey Resource bundle key for the alert dialog message.
288
   * @param e          The unexpected happening.
289
   * @return false
290
   */
291
  @SuppressWarnings("SameParameterValue")
292
  private boolean popupAlert(
293
      final String titleKey, final String messageKey, final Exception e ) {
294
    final Notifier service = getNotifier();
295
    final Path filePath = getPath();
296
297
    final Notification message = service.createNotification(
298
        get( titleKey ),
299
        get( messageKey ),
300
        filePath == null ? "" : filePath,
301
        e.getMessage()
302
    );
303
304
    try {
305
      service.createError( getWindow(), message ).showAndWait();
306
    } catch( final Exception ex ) {
307
      alert( ex );
308
    }
309
310
    return false;
311
  }
312
313
  private Window getWindow() {
314
    final Scene scene = getEditorPane().getScene();
315
316
    if( scene == null ) {
317
      throw new UnsupportedOperationException( "No scene window available" );
318
    }
319
320
    return scene.getWindow();
321
  }
322
323
  /**
324
   * Returns a best guess at the file encoding. If the encoding could not be
325
   * detected, this will return the default charset for the JVM.
326
   *
327
   * @param bytes The bytes to perform character encoding detection.
328
   * @return The character encoding.
329
   */
330
  private Charset detectEncoding( final byte[] bytes ) {
331
    final var detector = new UniversalDetector( null );
332
    detector.handleData( bytes, 0, bytes.length );
333
    detector.dataEnd();
334
335
    final String charset = detector.getDetectedCharset();
336
337
    return charset == null
338
        ? Charset.defaultCharset()
339
        : Charset.forName( charset.toUpperCase( ENGLISH ) );
340
  }
341
342
  /**
343
   * Converts the given string to an array of bytes using the encoding that was
344
   * originally detected (if any) and associated with this file.
345
   *
346
   * @param text The text to convert into the original file encoding.
347
   * @return A series of bytes ready for writing to a file.
348
   */
349
  private byte[] asBytes( final String text ) {
350
    return text.getBytes( getEncoding() );
351
  }
352
353
  /**
354
   * Converts the given bytes into a Java String. This will call setEncoding
355
   * with the encoding detected by the CharsetDetector.
356
   *
357
   * @param text The text of unknown character encoding.
358
   * @return The text, in its auto-detected encoding, as a String.
359
   */
360
  private String asString( final byte[] text ) {
361
    setEncoding( detectEncoding( text ) );
362
    return new String( text, getEncoding() );
363
  }
364
365
  /**
366
   * Returns the path to the file being edited in this tab.
367
   *
368
   * @return A non-null instance.
369
   */
370
  public Path getPath() {
371
    return mPath;
372
  }
373
374
  /**
375
   * Sets the path to a file for editing and then updates the tab with the
376
   * file contents.
377
   *
378
   * @param path A non-null instance.
379
   */
380
  public void setPath( final Path path ) {
381
    assert path != null;
382
    mPath = path;
383
384
    updateTab();
385
  }
386
387
  public boolean isModified() {
388
    return mModified.get();
389
  }
390
391
  ReadOnlyBooleanProperty modifiedProperty() {
392
    return mModified.getReadOnlyProperty();
393
  }
394
395
  BooleanProperty canUndoProperty() {
396
    return this.canUndo;
397
  }
398
399
  BooleanProperty canRedoProperty() {
400
    return this.canRedo;
401
  }
402
403
  private UndoManager<?> getUndoManager() {
404
    return getEditorPane().getUndoManager();
405
  }
406
407
  /**
408
   * Forwards to the editor pane's listeners for text change events.
409
   *
410
   * @param listener The listener to notify when the text changes.
411
   */
412
  public void addTextChangeListener( final ChangeListener<String> listener ) {
413
    getEditorPane().addTextChangeListener( listener );
414
  }
415
416
  /**
417
   * Forwards to the editor pane's listeners for caret change events.
418
   *
419
   * @param listener Notified when the caret position changes.
420
   */
421
  public void addCaretPositionListener(
422
      final ChangeListener<? super Integer> listener ) {
423
    getEditorPane().addCaretPositionListener( listener );
424
  }
425
426
  /**
427
   * Forwards to the editor pane's listeners for paragraph index change events.
428
   *
429
   * @param listener Notified when the caret's paragraph index changes.
430
   */
431
  public void addCaretParagraphListener(
432
      final ChangeListener<? super Integer> listener ) {
433
    getEditorPane().addCaretParagraphListener( listener );
434
  }
435
436
  public <T extends Event> void addEventFilter(
437
      final EventType<T> eventType,
438
      final EventHandler<? super T> eventFilter ) {
439
    getEditor().addEventFilter( eventType, eventFilter );
440
  }
441
442
  /**
443
   * Forwards the request to the editor pane.
444
   *
445
   * @return The text to process.
446
   */
447
  public String getEditorText() {
448
    return getEditorPane().getText();
449
  }
450
451
  /**
452
   * Returns the editor pane, or creates one if it doesn't yet exist.
453
   *
454
   * @return The editor pane, never null.
455
   */
456
  @NotNull
457
  public MarkdownEditorPane getEditorPane() {
458
    return mEditorPane;
459
  }
460
461
  /**
462
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
463
   * determined.
464
   *
465
   * @return The file encoding or UTF-8 if unknown.
466
   */
467
  private Charset getEncoding() {
468
    return mEncoding;
469
  }
470
471
  private void setEncoding( final Charset encoding ) {
472
    assert encoding != null;
473
    mEncoding = encoding;
474
  }
475
476
  /**
477
   * Returns the tab title, without any modified indicators.
478
   *
479
   * @return The tab title.
480
   */
481
  @Override
482
  public String toString() {
483
    return getTabTitle();
484
  }
485
}
4861
D src/main/java/com/scrivenvar/FileEditorTabPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.Options;
31
import com.scrivenvar.service.Settings;
32
import com.scrivenvar.service.events.Notification;
33
import com.scrivenvar.service.events.Notifier;
34
import com.scrivenvar.util.Utils;
35
import javafx.beans.property.ReadOnlyBooleanProperty;
36
import javafx.beans.property.ReadOnlyBooleanWrapper;
37
import javafx.beans.property.ReadOnlyObjectProperty;
38
import javafx.beans.property.ReadOnlyObjectWrapper;
39
import javafx.beans.value.ChangeListener;
40
import javafx.collections.ListChangeListener;
41
import javafx.collections.ObservableList;
42
import javafx.event.Event;
43
import javafx.scene.control.Alert;
44
import javafx.scene.control.ButtonType;
45
import javafx.scene.control.Tab;
46
import javafx.scene.control.TabPane;
47
import javafx.stage.FileChooser;
48
import javafx.stage.FileChooser.ExtensionFilter;
49
import javafx.stage.Window;
50
51
import java.io.File;
52
import java.nio.file.Path;
53
import java.util.ArrayList;
54
import java.util.List;
55
import java.util.Optional;
56
import java.util.concurrent.atomic.AtomicReference;
57
import java.util.prefs.Preferences;
58
import java.util.stream.Collectors;
59
60
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
61
import static com.scrivenvar.Constants.SETTINGS;
62
import static com.scrivenvar.FileType.*;
63
import static com.scrivenvar.Messages.get;
64
import static com.scrivenvar.predicates.PredicateFactory.createFileTypePredicate;
65
import static com.scrivenvar.service.events.Notifier.YES;
66
67
/**
68
 * Tab pane for file editors.
69
 */
70
public final class FileEditorTabPane extends TabPane {
71
72
  private static final String FILTER_EXTENSION_TITLES =
73
      "Dialog.file.choose.filter";
74
75
  private static final Options sOptions = Services.load( Options.class );
76
  private static final Notifier sNotifier = Services.load( Notifier.class );
77
78
  private final ReadOnlyObjectWrapper<Path> mOpenDefinition =
79
      new ReadOnlyObjectWrapper<>();
80
  private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
81
      new ReadOnlyObjectWrapper<>();
82
  private final ReadOnlyBooleanWrapper mAnyFileEditorModified =
83
      new ReadOnlyBooleanWrapper();
84
  private final ChangeListener<Integer> mCaretPositionListener;
85
  private final ChangeListener<Integer> mCaretParagraphListener;
86
87
  /**
88
   * Constructs a new file editor tab pane.
89
   *
90
   * @param caretPositionListener  Listens for changes to caret position so
91
   *                               that the status bar can update.
92
   * @param caretParagraphListener Listens for changes to the caret's paragraph
93
   *                               so that scrolling may occur.
94
   */
95
  public FileEditorTabPane(
96
      final ChangeListener<Integer> caretPositionListener,
97
      final ChangeListener<Integer> caretParagraphListener ) {
98
    final ObservableList<Tab> tabs = getTabs();
99
100
    setFocusTraversable( false );
101
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
102
103
    addTabSelectionListener(
104
        ( tabPane, oldTab, newTab ) -> {
105
          if( newTab != null ) {
106
            mActiveFileEditor.set( (FileEditorTab) newTab );
107
          }
108
        }
109
    );
110
111
    final ChangeListener<Boolean> modifiedListener =
112
        ( observable, oldValue, newValue ) -> {
113
          for( final Tab tab : tabs ) {
114
            if( ((FileEditorTab) tab).isModified() ) {
115
              mAnyFileEditorModified.set( true );
116
              break;
117
            }
118
          }
119
        };
120
121
    tabs.addListener(
122
        (ListChangeListener<Tab>) change -> {
123
          while( change.next() ) {
124
            if( change.wasAdded() ) {
125
              change.getAddedSubList().forEach(
126
                  ( tab ) -> {
127
                    final var fet = (FileEditorTab) tab;
128
                    fet.modifiedProperty().addListener( modifiedListener );
129
                  } );
130
            }
131
            else if( change.wasRemoved() ) {
132
              change.getRemoved().forEach(
133
                  ( tab ) -> {
134
                    final var fet = (FileEditorTab) tab;
135
                    fet.modifiedProperty().removeListener( modifiedListener );
136
                  }
137
              );
138
            }
139
          }
140
141
          // Changes in the tabs may also change anyFileEditorModified property
142
          // (e.g. closed modified file)
143
          modifiedListener.changed( null, null, null );
144
        }
145
    );
146
147
    mCaretPositionListener = caretPositionListener;
148
    mCaretParagraphListener = caretParagraphListener;
149
  }
150
151
  /**
152
   * Allows observers to be notified when the current file editor tab changes.
153
   *
154
   * @param listener The listener to notify of tab change events.
155
   */
156
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
157
    // Observe the tab so that when a new tab is opened or selected,
158
    // a notification is kicked off.
159
    getSelectionModel().selectedItemProperty().addListener( listener );
160
  }
161
162
  /**
163
   * Returns the tab that has keyboard focus.
164
   *
165
   * @return A non-null instance.
166
   */
167
  public FileEditorTab getActiveFileEditor() {
168
    return mActiveFileEditor.get();
169
  }
170
171
  /**
172
   * Returns the property corresponding to the tab that has focus.
173
   *
174
   * @return A non-null instance.
175
   */
176
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
177
    return mActiveFileEditor.getReadOnlyProperty();
178
  }
179
180
  /**
181
   * Property that can answer whether the text has been modified.
182
   *
183
   * @return A non-null instance, true meaning the content has not been saved.
184
   */
185
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
186
    return mAnyFileEditorModified.getReadOnlyProperty();
187
  }
188
189
  /**
190
   * Creates a new editor instance from the given path.
191
   *
192
   * @param path The file to open.
193
   * @return A non-null instance.
194
   */
195
  private FileEditorTab createFileEditor( final Path path ) {
196
    assert path != null;
197
198
    final FileEditorTab tab = new FileEditorTab( path );
199
200
    tab.setOnCloseRequest( e -> {
201
      if( !canCloseEditor( tab ) ) {
202
        e.consume();
203
      }
204
      else if( isActiveFileEditor( tab ) ) {
205
        // Prevent prompting the user to save when there are no file editor
206
        // tabs open.
207
        mActiveFileEditor.set( null );
208
      }
209
    } );
210
211
    tab.addCaretPositionListener( mCaretPositionListener );
212
    tab.addCaretParagraphListener( mCaretParagraphListener );
213
214
    return tab;
215
  }
216
217
  private boolean isActiveFileEditor( final FileEditorTab tab ) {
218
    return getActiveFileEditor() == tab;
219
  }
220
221
  private Path getDefaultPath() {
222
    final String filename = getDefaultFilename();
223
    return (new File( filename )).toPath();
224
  }
225
226
  private String getDefaultFilename() {
227
    return getSettings().getSetting( "file.default", "untitled.md" );
228
  }
229
230
  /**
231
   * Called to add a new {@link FileEditorTab} to the tab pane.
232
   */
233
  void newEditor() {
234
    final FileEditorTab tab = createFileEditor( getDefaultPath() );
235
236
    getTabs().add( tab );
237
    getSelectionModel().select( tab );
238
  }
239
240
  void openFileDialog() {
241
    final String title = get( "Dialog.file.choose.open.title" );
242
    final FileChooser dialog = createFileChooser( title );
243
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
244
245
    if( files != null ) {
246
      openFiles( files );
247
    }
248
  }
249
250
  /**
251
   * Opens the files into new editors, unless one of those files was a
252
   * definition file. The definition file is loaded into the definition pane,
253
   * but only the first one selected (multiple definition files will result in a
254
   * warning).
255
   *
256
   * @param files The list of non-definition files that the were requested to
257
   *              open.
258
   */
259
  private void openFiles( final List<File> files ) {
260
    final List<String> extensions =
261
        createExtensionFilter( DEFINITION ).getExtensions();
262
    final var predicate = createFileTypePredicate( extensions );
263
264
    // The user might have opened multiple definitions files. These will
265
    // be discarded from the text editable files.
266
    final var definitions
267
        = files.stream().filter( predicate ).collect( Collectors.toList() );
268
269
    // Create a modifiable list to remove any definition files that were
270
    // opened.
271
    final var editors = new ArrayList<>( files );
272
273
    if( !editors.isEmpty() ) {
274
      saveLastDirectory( editors.get( 0 ) );
275
    }
276
277
    editors.removeAll( definitions );
278
279
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
280
    if( !editors.isEmpty() ) {
281
      openEditors( editors, 0 );
282
    }
283
284
    if( !definitions.isEmpty() ) {
285
      openDefinition( definitions.get( 0 ) );
286
    }
287
  }
288
289
  private void openEditors( final List<File> files, final int activeIndex ) {
290
    final int fileTally = files.size();
291
    final List<Tab> tabs = getTabs();
292
293
    // Close single unmodified "Untitled" tab.
294
    if( tabs.size() == 1 ) {
295
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
296
297
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
298
        closeEditor( fileEditor, false );
299
      }
300
    }
301
302
    for( int i = 0; i < fileTally; i++ ) {
303
      final Path path = files.get( i ).toPath();
304
305
      FileEditorTab fileEditorTab = findEditor( path );
306
307
      // Only open new files.
308
      if( fileEditorTab == null ) {
309
        fileEditorTab = createFileEditor( path );
310
        getTabs().add( fileEditorTab );
311
      }
312
313
      // Select the first file in the list.
314
      if( i == activeIndex ) {
315
        getSelectionModel().select( fileEditorTab );
316
      }
317
    }
318
  }
319
320
  /**
321
   * Returns a property that changes when a new definition file is opened.
322
   *
323
   * @return The path to a definition file that was opened.
324
   */
325
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
326
    return getOnOpenDefinitionFile().getReadOnlyProperty();
327
  }
328
329
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
330
    return mOpenDefinition;
331
  }
332
333
  /**
334
   * Called when the user has opened a definition file (using the file open
335
   * dialog box). This will replace the current set of definitions for the
336
   * active tab.
337
   *
338
   * @param definition The file to open.
339
   */
340
  private void openDefinition( final File definition ) {
341
    // TODO: Prevent reading this file twice when a new text document is opened.
342
    // (might be a matter of checking the value first).
343
    getOnOpenDefinitionFile().set( definition.toPath() );
344
  }
345
346
  /**
347
   * Called when the contents of the editor are to be saved.
348
   *
349
   * @param tab The tab containing content to save.
350
   * @return true The contents were saved (or needn't be saved).
351
   */
352
  public boolean saveEditor( final FileEditorTab tab ) {
353
    if( tab == null || !tab.isModified() ) {
354
      return true;
355
    }
356
357
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
358
  }
359
360
  /**
361
   * Opens the Save As dialog for the user to save the content under a new
362
   * path.
363
   *
364
   * @param tab The tab with contents to save.
365
   * @return true The contents were saved, or the tab was null.
366
   */
367
  public boolean saveEditorAs( final FileEditorTab tab ) {
368
    if( tab == null ) {
369
      return true;
370
    }
371
372
    getSelectionModel().select( tab );
373
374
    final FileChooser fileChooser = createFileChooser( get(
375
        "Dialog.file.choose.save.title" ) );
376
    final File file = fileChooser.showSaveDialog( getWindow() );
377
    if( file == null ) {
378
      return false;
379
    }
380
381
    saveLastDirectory( file );
382
    tab.setPath( file.toPath() );
383
384
    return tab.save();
385
  }
386
387
  void saveAllEditors() {
388
    for( final FileEditorTab fileEditor : getAllEditors() ) {
389
      saveEditor( fileEditor );
390
    }
391
  }
392
393
  /**
394
   * Answers whether the file has had modifications. '
395
   *
396
   * @param tab THe tab to check for modifications.
397
   * @return false The file is unmodified.
398
   */
399
  @SuppressWarnings("BooleanMethodIsAlwaysInverted")
400
  boolean canCloseEditor( final FileEditorTab tab ) {
401
    final AtomicReference<Boolean> canClose = new AtomicReference<>();
402
    canClose.set( true );
403
404
    if( tab.isModified() ) {
405
      final Notification message = getNotifyService().createNotification(
406
          Messages.get( "Alert.file.close.title" ),
407
          Messages.get( "Alert.file.close.text" ),
408
          tab.getText()
409
      );
410
411
      final Alert confirmSave = getNotifyService().createConfirmation(
412
          getWindow(), message );
413
414
      final Optional<ButtonType> buttonType = confirmSave.showAndWait();
415
416
      buttonType.ifPresent(
417
          save -> canClose.set(
418
              save == YES ? saveEditor( tab ) : save == ButtonType.NO
419
          )
420
      );
421
    }
422
423
    return canClose.get();
424
  }
425
426
  boolean closeEditor( final FileEditorTab tab, final boolean save ) {
427
    if( tab == null ) {
428
      return true;
429
    }
430
431
    if( save ) {
432
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
433
      Event.fireEvent( tab, event );
434
435
      if( event.isConsumed() ) {
436
        return false;
437
      }
438
    }
439
440
    getTabs().remove( tab );
441
442
    if( tab.getOnClosed() != null ) {
443
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
444
    }
445
446
    return true;
447
  }
448
449
  boolean closeAllEditors() {
450
    final FileEditorTab[] allEditors = getAllEditors();
451
    final FileEditorTab activeEditor = getActiveFileEditor();
452
453
    // try to save active tab first because in case the user decides to cancel,
454
    // then it stays active
455
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
456
      return false;
457
    }
458
459
    // This should be called any time a tab changes.
460
    persistPreferences();
461
462
    // save modified tabs
463
    for( int i = 0; i < allEditors.length; i++ ) {
464
      final FileEditorTab fileEditor = allEditors[ i ];
465
466
      if( fileEditor == activeEditor ) {
467
        continue;
468
      }
469
470
      if( fileEditor.isModified() ) {
471
        // activate the modified tab to make its modified content visible to
472
        // the user
473
        getSelectionModel().select( i );
474
475
        if( !canCloseEditor( fileEditor ) ) {
476
          return false;
477
        }
478
      }
479
    }
480
481
    // Close all tabs.
482
    for( final FileEditorTab fileEditor : allEditors ) {
483
      if( !closeEditor( fileEditor, false ) ) {
484
        return false;
485
      }
486
    }
487
488
    return getTabs().isEmpty();
489
  }
490
491
  private FileEditorTab[] getAllEditors() {
492
    final ObservableList<Tab> tabs = getTabs();
493
    final int length = tabs.size();
494
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
495
496
    for( int i = 0; i < length; i++ ) {
497
      allEditors[ i ] = (FileEditorTab) tabs.get( i );
498
    }
499
500
    return allEditors;
501
  }
502
503
  /**
504
   * Returns the file editor tab that has the given path.
505
   *
506
   * @return null No file editor tab for the given path was found.
507
   */
508
  private FileEditorTab findEditor( final Path path ) {
509
    for( final Tab tab : getTabs() ) {
510
      final FileEditorTab fileEditor = (FileEditorTab) tab;
511
512
      if( fileEditor.isPath( path ) ) {
513
        return fileEditor;
514
      }
515
    }
516
517
    return null;
518
  }
519
520
  private FileChooser createFileChooser( String title ) {
521
    final FileChooser fileChooser = new FileChooser();
522
523
    fileChooser.setTitle( title );
524
    fileChooser.getExtensionFilters().addAll(
525
        createExtensionFilters() );
526
527
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
528
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
529
530
    if( !file.isDirectory() ) {
531
      file = new File( "." );
532
    }
533
534
    fileChooser.setInitialDirectory( file );
535
    return fileChooser;
536
  }
537
538
  private List<ExtensionFilter> createExtensionFilters() {
539
    final List<ExtensionFilter> list = new ArrayList<>();
540
541
    // TODO: Return a list of all properties that match the filter prefix.
542
    // This will allow dynamic filters to be added and removed just by
543
    // updating the properties file.
544
    list.add( createExtensionFilter( ALL ) );
545
    list.add( createExtensionFilter( SOURCE ) );
546
    list.add( createExtensionFilter( DEFINITION ) );
547
    list.add( createExtensionFilter( XML ) );
548
    return list;
549
  }
550
551
  /**
552
   * Returns a filter for file name extensions recognized by the application
553
   * that can be opened by the user.
554
   *
555
   * @param filetype Used to find the globbing pattern for extensions.
556
   * @return A filename filter suitable for use by a FileDialog instance.
557
   */
558
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
559
    final String tKey = String.format( "%s.title.%s",
560
                                       FILTER_EXTENSION_TITLES,
561
                                       filetype );
562
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
563
564
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
565
  }
566
567
  private void saveLastDirectory( final File file ) {
568
    getPreferences().put( "lastDirectory", file.getParent() );
569
  }
570
571
  public void initPreferences() {
572
    int activeIndex = 0;
573
574
    final Preferences preferences = getPreferences();
575
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
576
    final String activeFileName = preferences.get( "activeFile", null );
577
578
    final List<File> files = new ArrayList<>( fileNames.length );
579
580
    for( final String fileName : fileNames ) {
581
      final File file = new File( fileName );
582
583
      if( file.exists() ) {
584
        files.add( file );
585
586
        if( fileName.equals( activeFileName ) ) {
587
          activeIndex = files.size() - 1;
588
        }
589
      }
590
    }
591
592
    if( files.isEmpty() ) {
593
      newEditor();
594
    }
595
    else {
596
      openEditors( files, activeIndex );
597
    }
598
  }
599
600
  public void persistPreferences() {
601
    final var allEditors = getTabs();
602
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
603
604
    for( final var tab : allEditors ) {
605
      final var fileEditor = (FileEditorTab) tab;
606
      final var filePath = fileEditor.getPath();
607
608
      if( filePath != null ) {
609
        fileNames.add( filePath.toString() );
610
      }
611
    }
612
613
    final var preferences = getPreferences();
614
    Utils.putPrefsStrings( preferences,
615
                           "file",
616
                           fileNames.toArray( new String[ 0 ] ) );
617
618
    final var activeEditor = getActiveFileEditor();
619
    final var filePath = activeEditor == null ? null : activeEditor.getPath();
620
621
    if( filePath == null ) {
622
      preferences.remove( "activeFile" );
623
    }
624
    else {
625
      preferences.put( "activeFile", filePath.toString() );
626
    }
627
  }
628
629
  private List<String> getExtensions( final String key ) {
630
    return getSettings().getStringSettingList( key );
631
  }
632
633
  private Notifier getNotifyService() {
634
    return sNotifier;
635
  }
636
637
  private Settings getSettings() {
638
    return SETTINGS;
639
  }
640
641
  protected Options getOptions() {
642
    return sOptions;
643
  }
644
645
  private Window getWindow() {
646
    return getScene().getWindow();
647
  }
648
649
  private Preferences getPreferences() {
650
    return getOptions().getState();
651
  }
652
}
6531
D src/main/java/com/scrivenvar/FileType.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
/**
31
 * Represents different file type classifications. These are high-level mappings
32
 * that correspond to the list of glob patterns found within {@code
33
 * settings.properties}.
34
 */
35
public enum FileType {
36
37
  ALL( "all" ),
38
  RMARKDOWN( "rmarkdown" ),
39
  RXML( "rxml" ),
40
  SOURCE( "source" ),
41
  DEFINITION( "definition" ),
42
  XML( "xml" ),
43
  CSV( "csv" ),
44
  JSON( "json" ),
45
  TOML( "toml" ),
46
  YAML( "yaml" ),
47
  PROPERTIES( "properties" ),
48
  UNKNOWN( "unknown" );
49
50
  private final String mType;
51
52
  /**
53
   * Default constructor for enumerated file type.
54
   *
55
   * @param type Human-readable name for the file type.
56
   */
57
  FileType( final String type ) {
58
    mType = type;
59
  }
60
61
  /**
62
   * Returns the file type that corresponds to the given string.
63
   *
64
   * @param type The string to compare against this enumeration of file types.
65
   * @return The corresponding File Type for the given string.
66
   * @throws IllegalArgumentException Type not found.
67
   */
68
  public static FileType from( final String type ) {
69
    for( final FileType fileType : FileType.values() ) {
70
      if( fileType.isType( type ) ) {
71
        return fileType;
72
      }
73
    }
74
75
    throw new IllegalArgumentException( type );
76
  }
77
78
  /**
79
   * Answers whether this file type matches the given string, case insensitive
80
   * comparison.
81
   *
82
   * @param type Presumably a file name extension to check against.
83
   * @return true The given extension corresponds to this enumerated type.
84
   */
85
  public boolean isType( final String type ) {
86
    return getType().equalsIgnoreCase( type );
87
  }
88
89
  /**
90
   * Returns the human-readable name for the file type.
91
   *
92
   * @return A non-null instance.
93
   */
94
  private String getType() {
95
    return mType;
96
  }
97
98
  /**
99
   * Returns the lowercase version of the file name extension.
100
   *
101
   * @return The file name, in lower case.
102
   */
103
  @Override
104
  public String toString() {
105
    return getType();
106
  }
107
}
1081
D src/main/java/com/scrivenvar/Launcher.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import java.io.IOException;
31
import java.io.InputStream;
32
import java.util.Calendar;
33
import java.util.Properties;
34
35
import static java.lang.String.format;
36
37
/**
38
 * Launches the application using the {@link Main} class.
39
 *
40
 * <p>
41
 * This is required until modules are implemented, which may never happen
42
 * because the application should be ported away from Java and JavaFX.
43
 * </p>
44
 */
45
public class Launcher {
46
  /**
47
   * Delegates to the application entry point.
48
   *
49
   * @param args Command-line arguments.
50
   */
51
  public static void main( final String[] args ) throws IOException {
52
    showAppInfo();
53
    Main.main( args );
54
  }
55
56
  @SuppressWarnings("RedundantStringFormatCall")
57
  private static void showAppInfo() throws IOException {
58
    out( format( "%s version %s", getTitle(), getVersion() ) );
59
    out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) );
60
    out( format( "Portions copyright 2020 Karl Tauber." ) );
61
  }
62
63
  private static void out( final String s ) {
64
    System.out.println( s );
65
  }
66
67
  private static String getTitle() throws IOException {
68
    final Properties properties = loadProperties( "messages.properties" );
69
    return properties.getProperty( "Main.title" );
70
  }
71
72
  private static String getVersion() throws IOException {
73
    final Properties properties = loadProperties( "app.properties" );
74
    return properties.getProperty( "application.version" );
75
  }
76
77
  private static String getYear() {
78
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
79
  }
80
81
  @SuppressWarnings("SameParameterValue")
82
  private static Properties loadProperties( final String resource )
83
      throws IOException {
84
    final Properties properties = new Properties();
85
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
86
    return properties;
87
  }
88
89
  private static String getResourceName( final String resource ) {
90
    return format( "%s/%s", getPackagePath(), resource );
91
  }
92
93
  private static String getPackagePath() {
94
    return Launcher.class.getPackageName().replace( '.', '/' );
95
  }
96
97
  private static InputStream getResourceAsStream( final String resource ) {
98
    return Launcher.class.getClassLoader().getResourceAsStream( resource );
99
  }
100
}
1011
D src/main/java/com/scrivenvar/Main.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.preferences.FilePreferencesFactory;
31
import com.scrivenvar.service.Options;
32
import com.scrivenvar.service.Snitch;
33
import com.scrivenvar.util.ResourceWalker;
34
import com.scrivenvar.util.StageState;
35
import javafx.application.Application;
36
import javafx.scene.Scene;
37
import javafx.scene.image.Image;
38
import javafx.stage.Stage;
39
40
import java.awt.*;
41
import java.io.FileInputStream;
42
import java.io.IOException;
43
import java.io.InputStream;
44
import java.net.URI;
45
import java.util.Map;
46
import java.util.logging.LogManager;
47
48
import static com.scrivenvar.Constants.*;
49
import static com.scrivenvar.Messages.get;
50
import static com.scrivenvar.StatusBarNotifier.alert;
51
import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment;
52
import static java.awt.font.TextAttribute.*;
53
import static javafx.scene.input.KeyCode.F11;
54
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
55
56
/**
57
 * Application entry point. The application allows users to edit Markdown
58
 * files and see a real-time preview of the edits.
59
 */
60
public final class Main extends Application {
61
62
  static {
63
    // Suppress logging to standard output.
64
    LogManager.getLogManager().reset();
65
66
    // Suppress logging to standard error.
67
    System.err.close();
68
  }
69
70
  private final Options mOptions = Services.load( Options.class );
71
  private final Snitch mSnitch = Services.load( Snitch.class );
72
73
  private final Thread mSnitchThread = new Thread( getSnitch() );
74
  private final MainWindow mMainWindow = new MainWindow();
75
76
  @SuppressWarnings({"FieldCanBeLocal", "unused"})
77
  private StageState mStageState;
78
79
  /**
80
   * Application entry point.
81
   *
82
   * @param args Command-line arguments.
83
   */
84
  public static void main( final String[] args ) {
85
    initPreferences();
86
    initFonts();
87
    launch( args );
88
  }
89
90
  /**
91
   * JavaFX entry point.
92
   *
93
   * @param stage The primary application stage.
94
   */
95
  @Override
96
  public void start( final Stage stage ) {
97
    initState( stage );
98
    initStage( stage );
99
    initSnitch();
100
101
    stage.show();
102
103
    // After the stage is visible, the panel dimensions are
104
    // known, which allows scaling images to fit the preview panel.
105
    getMainWindow().init();
106
  }
107
108
  /**
109
   * This needs to run before the windowing system kicks in, otherwise the
110
   * fonts will not be found.
111
   */
112
  @SuppressWarnings({"rawtypes", "unchecked"})
113
  public static void initFonts() {
114
    final var ge = getLocalGraphicsEnvironment();
115
116
    try {
117
      ResourceWalker.walk(
118
          FONT_DIRECTORY, path -> {
119
            final var uri = path.toUri();
120
            final var filename = path.toString();
121
122
            try( final var is = openFont( uri, filename ) ) {
123
              final var font = Font.createFont( Font.TRUETYPE_FONT, is );
124
              final Map attributes = font.getAttributes();
125
126
              attributes.put( LIGATURES, LIGATURES_ON );
127
              attributes.put( KERNING, KERNING_ON );
128
              ge.registerFont( font.deriveFont( attributes ) );
129
            } catch( final Exception e ) {
130
              alert( e );
131
            }
132
          }
133
      );
134
    } catch( final Exception e ) {
135
      alert( e );
136
    }
137
  }
138
139
  private static InputStream openFont( final URI uri, final String filename )
140
      throws IOException {
141
    return uri.getScheme().equals( "jar" )
142
        ? Main.class.getResourceAsStream( filename )
143
        : new FileInputStream( filename );
144
  }
145
146
  /**
147
   * Sets the factory used for reading user preferences.
148
   */
149
  private static void initPreferences() {
150
    System.setProperty(
151
        "java.util.prefs.PreferencesFactory",
152
        FilePreferencesFactory.class.getName()
153
    );
154
  }
155
156
  private void initState( final Stage stage ) {
157
    mStageState = new StageState( stage, getOptions().getState() );
158
  }
159
160
  private void initStage( final Stage stage ) {
161
    stage.getIcons().addAll(
162
        createImage( FILE_LOGO_16 ),
163
        createImage( FILE_LOGO_32 ),
164
        createImage( FILE_LOGO_128 ),
165
        createImage( FILE_LOGO_256 ),
166
        createImage( FILE_LOGO_512 ) );
167
    stage.setTitle( getApplicationTitle() );
168
    stage.setScene( getScene() );
169
170
    stage.addEventHandler( KEY_PRESSED, event -> {
171
      if( F11.equals( event.getCode() ) ) {
172
        stage.setFullScreen( !stage.isFullScreen() );
173
      }
174
    } );
175
  }
176
177
  /**
178
   * Watch for file system changes.
179
   */
180
  private void initSnitch() {
181
    getSnitchThread().start();
182
  }
183
184
  /**
185
   * Stops the snitch service, if its running.
186
   *
187
   * @throws InterruptedException Couldn't stop the snitch thread.
188
   */
189
  @Override
190
  public void stop() throws InterruptedException {
191
    getSnitch().stop();
192
193
    final Thread thread = getSnitchThread();
194
    thread.interrupt();
195
    thread.join();
196
  }
197
198
  private Snitch getSnitch() {
199
    return mSnitch;
200
  }
201
202
  private Thread getSnitchThread() {
203
    return mSnitchThread;
204
  }
205
206
  private Options getOptions() {
207
    return mOptions;
208
  }
209
210
  private MainWindow getMainWindow() {
211
    return mMainWindow;
212
  }
213
214
  private Scene getScene() {
215
    return getMainWindow().getScene();
216
  }
217
218
  private String getApplicationTitle() {
219
    return get( "Main.title" );
220
  }
221
222
  private Image createImage( final String filename ) {
223
    return new Image( filename );
224
  }
225
}
2261
D src/main/java/com/scrivenvar/MainWindow.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.dlsc.preferencesfx.PreferencesFxEvent;
31
import com.scrivenvar.definition.DefinitionFactory;
32
import com.scrivenvar.definition.DefinitionPane;
33
import com.scrivenvar.definition.DefinitionSource;
34
import com.scrivenvar.definition.MapInterpolator;
35
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
36
import com.scrivenvar.editors.DefinitionNameInjector;
37
import com.scrivenvar.editors.EditorPane;
38
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
39
import com.scrivenvar.preferences.UserPreferences;
40
import com.scrivenvar.preview.HTMLPreviewPane;
41
import com.scrivenvar.processors.HtmlPreviewProcessor;
42
import com.scrivenvar.processors.Processor;
43
import com.scrivenvar.processors.ProcessorFactory;
44
import com.scrivenvar.service.Options;
45
import com.scrivenvar.service.Snitch;
46
import com.scrivenvar.spelling.api.SpellCheckListener;
47
import com.scrivenvar.spelling.api.SpellChecker;
48
import com.scrivenvar.spelling.impl.PermissiveSpeller;
49
import com.scrivenvar.spelling.impl.SymSpellSpeller;
50
import com.scrivenvar.util.Action;
51
import com.scrivenvar.util.ActionBuilder;
52
import com.scrivenvar.util.ActionUtils;
53
import com.vladsch.flexmark.parser.Parser;
54
import com.vladsch.flexmark.util.ast.NodeVisitor;
55
import com.vladsch.flexmark.util.ast.VisitHandler;
56
import javafx.beans.binding.Bindings;
57
import javafx.beans.binding.BooleanBinding;
58
import javafx.beans.property.BooleanProperty;
59
import javafx.beans.property.SimpleBooleanProperty;
60
import javafx.beans.value.ChangeListener;
61
import javafx.beans.value.ObservableBooleanValue;
62
import javafx.beans.value.ObservableValue;
63
import javafx.collections.ListChangeListener.Change;
64
import javafx.collections.ObservableList;
65
import javafx.event.Event;
66
import javafx.event.EventHandler;
67
import javafx.geometry.Pos;
68
import javafx.scene.Node;
69
import javafx.scene.Scene;
70
import javafx.scene.control.*;
71
import javafx.scene.control.Alert.AlertType;
72
import javafx.scene.image.Image;
73
import javafx.scene.image.ImageView;
74
import javafx.scene.input.Clipboard;
75
import javafx.scene.input.ClipboardContent;
76
import javafx.scene.input.KeyEvent;
77
import javafx.scene.layout.BorderPane;
78
import javafx.scene.layout.VBox;
79
import javafx.scene.text.Text;
80
import javafx.stage.Window;
81
import javafx.stage.WindowEvent;
82
import javafx.util.Duration;
83
import org.apache.commons.lang3.SystemUtils;
84
import org.controlsfx.control.StatusBar;
85
import org.fxmisc.richtext.StyleClassedTextArea;
86
import org.fxmisc.richtext.model.StyleSpansBuilder;
87
import org.reactfx.value.Val;
88
89
import java.io.BufferedReader;
90
import java.io.FileNotFoundException;
91
import java.io.InputStreamReader;
92
import java.nio.file.Path;
93
import java.util.*;
94
import java.util.concurrent.atomic.AtomicInteger;
95
import java.util.function.Consumer;
96
import java.util.function.Function;
97
import java.util.prefs.Preferences;
98
import java.util.stream.Collectors;
99
100
import static com.scrivenvar.Constants.*;
101
import static com.scrivenvar.Messages.get;
102
import static com.scrivenvar.StatusBarNotifier.alert;
103
import static com.scrivenvar.util.StageState.*;
104
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
105
import static java.nio.charset.StandardCharsets.UTF_8;
106
import static java.util.Collections.emptyList;
107
import static java.util.Collections.singleton;
108
import static javafx.application.Platform.runLater;
109
import static javafx.event.Event.fireEvent;
110
import static javafx.scene.input.KeyCode.ENTER;
111
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
112
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
113
114
/**
115
 * Main window containing a tab pane in the center for file editors.
116
 */
117
public class MainWindow implements Observer {
118
  /**
119
   * The {@code OPTIONS} variable must be declared before all other variables
120
   * to prevent subsequent initializations from failing due to missing user
121
   * preferences.
122
   */
123
  private static final Options sOptions = Services.load( Options.class );
124
  private static final Snitch SNITCH = Services.load( Snitch.class );
125
126
  private final Scene mScene;
127
  private final StatusBar mStatusBar;
128
  private final Text mLineNumberText;
129
  private final TextField mFindTextField;
130
  private final SpellChecker mSpellChecker;
131
132
  private final Object mMutex = new Object();
133
134
  /**
135
   * Prevents re-instantiation of processing classes.
136
   */
137
  private final Map<FileEditorTab, Processor<String>> mProcessors =
138
      new HashMap<>();
139
140
  private final Map<String, String> mResolvedMap =
141
      new HashMap<>( DEFAULT_MAP_SIZE );
142
143
  private final EventHandler<PreferencesFxEvent> mRPreferencesListener =
144
      event -> rerender();
145
146
  /**
147
   * Called when the definition data is changed.
148
   */
149
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
150
      mTreeHandler = event -> {
151
    exportDefinitions( getDefinitionPath() );
152
    interpolateResolvedMap();
153
    rerender();
154
  };
155
156
  /**
157
   * Called to inject the selected item when the user presses ENTER in the
158
   * definition pane.
159
   */
160
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
161
      event -> {
162
        if( event.getCode() == ENTER ) {
163
          getDefinitionNameInjector().injectSelectedItem();
164
        }
165
      };
166
167
  private final ChangeListener<Integer> mCaretPositionListener =
168
      ( observable, oldPosition, newPosition ) -> {
169
        final FileEditorTab tab = getActiveFileEditorTab();
170
        final EditorPane pane = tab.getEditorPane();
171
        final StyleClassedTextArea editor = pane.getEditor();
172
173
        getLineNumberText().setText(
174
            get( STATUS_BAR_LINE,
175
                 editor.getCurrentParagraph() + 1,
176
                 editor.getParagraphs().size(),
177
                 editor.getCaretPosition()
178
            )
179
        );
180
      };
181
182
  private final ChangeListener<Integer> mCaretParagraphListener =
183
      ( observable, oldIndex, newIndex ) ->
184
          scrollToParagraph( newIndex, true );
185
186
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
187
  private final DefinitionPane mDefinitionPane = createDefinitionPane();
188
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
189
  private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
190
      mCaretPositionListener,
191
      mCaretParagraphListener );
192
193
  /**
194
   * Listens on the definition pane for double-click events.
195
   */
196
  private final DefinitionNameInjector mDefinitionNameInjector
197
      = new DefinitionNameInjector( mDefinitionPane );
198
199
  public MainWindow() {
200
    mStatusBar = createStatusBar();
201
    mLineNumberText = createLineNumberText();
202
    mFindTextField = createFindTextField();
203
    mScene = createScene();
204
    mSpellChecker = createSpellChecker();
205
206
    // Add the close request listener before the window is shown.
207
    initLayout();
208
    StatusBarNotifier.setStatusBar( mStatusBar );
209
  }
210
211
  /**
212
   * Called after the stage is shown.
213
   */
214
  public void init() {
215
    initFindInput();
216
    initSnitch();
217
    initDefinitionListener();
218
    initTabAddedListener();
219
    initTabChangedListener();
220
    initPreferences();
221
    initVariableNameInjector();
222
  }
223
224
  private void initLayout() {
225
    final var scene = getScene();
226
227
    scene.getStylesheets().add( STYLESHEET_SCENE );
228
    scene.windowProperty().addListener(
229
        ( unused, oldWindow, newWindow ) ->
230
            newWindow.setOnCloseRequest(
231
                e -> {
232
                  if( !getFileEditorPane().closeAllEditors() ) {
233
                    e.consume();
234
                  }
235
                }
236
            )
237
    );
238
  }
239
240
  /**
241
   * Initialize the find input text field to listen on F3, ENTER, and
242
   * ESCAPE key presses.
243
   */
244
  private void initFindInput() {
245
    final TextField input = getFindTextField();
246
247
    input.setOnKeyPressed( ( KeyEvent event ) -> {
248
      switch( event.getCode() ) {
249
        case F3:
250
        case ENTER:
251
          editFindNext();
252
          break;
253
        case F:
254
          if( !event.isControlDown() ) {
255
            break;
256
          }
257
        case ESCAPE:
258
          getStatusBar().setGraphic( null );
259
          getActiveFileEditorTab().getEditorPane().requestFocus();
260
          break;
261
      }
262
    } );
263
264
    // Remove when the input field loses focus.
265
    input.focusedProperty().addListener(
266
        ( focused, oldFocus, newFocus ) -> {
267
          if( !newFocus ) {
268
            getStatusBar().setGraphic( null );
269
          }
270
        }
271
    );
272
  }
273
274
  /**
275
   * Watch for changes to external files. In particular, this awaits
276
   * modifications to any XSL files associated with XML files being edited.
277
   * When
278
   * an XSL file is modified (external to the application), the snitch's ears
279
   * perk up and the file is reloaded. This keeps the XSL transformation up to
280
   * date with what's on the file system.
281
   */
282
  private void initSnitch() {
283
    SNITCH.addObserver( this );
284
  }
285
286
  /**
287
   * Listen for {@link FileEditorTabPane} to receive open definition file
288
   * event.
289
   */
290
  private void initDefinitionListener() {
291
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
292
        ( final ObservableValue<? extends Path> file,
293
          final Path oldPath, final Path newPath ) -> {
294
          openDefinitions( newPath );
295
          rerender();
296
        }
297
    );
298
  }
299
300
  /**
301
   * Re-instantiates all processors then re-renders the active tab. This
302
   * will refresh the resolved map, force R to re-initialize, and brute-force
303
   * XSLT file reloads.
304
   */
305
  private void rerender() {
306
    runLater(
307
        () -> {
308
          resetProcessors();
309
          renderActiveTab();
310
        }
311
    );
312
  }
313
314
  /**
315
   * When tabs are added, hook the various change listeners onto the new
316
   * tab sothat the preview pane refreshes as necessary.
317
   */
318
  private void initTabAddedListener() {
319
    final FileEditorTabPane editorPane = getFileEditorPane();
320
321
    // Make sure the text processor kicks off when new files are opened.
322
    final ObservableList<Tab> tabs = editorPane.getTabs();
323
324
    // Update the preview pane on tab changes.
325
    tabs.addListener(
326
        ( final Change<? extends Tab> change ) -> {
327
          while( change.next() ) {
328
            if( change.wasAdded() ) {
329
              // Multiple tabs can be added simultaneously.
330
              for( final Tab newTab : change.getAddedSubList() ) {
331
                final FileEditorTab tab = (FileEditorTab) newTab;
332
333
                initTextChangeListener( tab );
334
                initScrollEventListener( tab );
335
                initSpellCheckListener( tab );
336
//              initSyntaxListener( tab );
337
              }
338
            }
339
          }
340
        }
341
    );
342
  }
343
344
  private void initTextChangeListener( final FileEditorTab tab ) {
345
    tab.addTextChangeListener(
346
        ( __, ov, nv ) -> {
347
          process( tab );
348
          scrollToParagraph( getCurrentParagraphIndex() );
349
        }
350
    );
351
  }
352
353
  private void initScrollEventListener( final FileEditorTab tab ) {
354
    final var scrollPane = tab.getScrollPane();
355
    final var scrollBar = getPreviewPane().getVerticalScrollBar();
356
357
    addShowListener( scrollPane, ( __ ) -> {
358
      final var handler = new ScrollEventHandler( scrollPane, scrollBar );
359
      handler.enabledProperty().bind( tab.selectedProperty() );
360
    } );
361
  }
362
363
  /**
364
   * Listen for changes to the any particular paragraph and perform a quick
365
   * spell check upon it. The style classes in the editor will be changed to
366
   * mark any spelling mistakes in the paragraph. The user may then interact
367
   * with any misspelled word (i.e., any piece of text that is marked) to
368
   * revise the spelling.
369
   *
370
   * @param tab The tab to spellcheck.
371
   */
372
  private void initSpellCheckListener( final FileEditorTab tab ) {
373
    final var editor = tab.getEditorPane().getEditor();
374
375
    // When the editor first appears, run a full spell check. This allows
376
    // spell checking while typing to be restricted to the active paragraph,
377
    // which is usually substantially smaller than the whole document.
378
    addShowListener(
379
        editor, ( __ ) -> spellcheck( editor, editor.getText() )
380
    );
381
382
    // Use the plain text changes so that notifications of style changes
383
    // are suppressed. Checking against the identity ensures that only
384
    // new text additions or deletions trigger proofreading.
385
    editor.plainTextChanges()
386
          .filter( p -> !p.isIdentity() ).subscribe( change -> {
387
388
      // Only perform a spell check on the current paragraph. The
389
      // entire document is processed once, when opened.
390
      final var offset = change.getPosition();
391
      final var position = editor.offsetToPosition( offset, Forward );
392
      final var paraId = position.getMajor();
393
      final var paragraph = editor.getParagraph( paraId );
394
      final var text = paragraph.getText();
395
396
      // Ensure that styles aren't doubled-up.
397
      editor.clearStyle( paraId );
398
399
      spellcheck( editor, text, paraId );
400
    } );
401
  }
402
403
  /**
404
   * Listen for new tab selection events.
405
   */
406
  private void initTabChangedListener() {
407
    final FileEditorTabPane editorPane = getFileEditorPane();
408
409
    // Update the preview pane changing tabs.
410
    editorPane.addTabSelectionListener(
411
        ( tabPane, oldTab, newTab ) -> {
412
          if( newTab == null ) {
413
            // Clear the preview pane when closing an editor. When the last
414
            // tab is closed, this ensures that the preview pane is empty.
415
            getPreviewPane().clear();
416
          }
417
          else {
418
            final var tab = (FileEditorTab) newTab;
419
            updateVariableNameInjector( tab );
420
            process( tab );
421
          }
422
        }
423
    );
424
  }
425
426
  /**
427
   * Reloads the preferences from the previous session.
428
   */
429
  private void initPreferences() {
430
    initDefinitionPane();
431
    getFileEditorPane().initPreferences();
432
    getUserPreferences().addSaveEventHandler( mRPreferencesListener );
433
  }
434
435
  private void initVariableNameInjector() {
436
    updateVariableNameInjector( getActiveFileEditorTab() );
437
  }
438
439
  /**
440
   * Calls the listener when the given node is shown for the first time. The
441
   * visible property is not the same as the initial showing event; visibility
442
   * can be triggered numerous times (such as going off screen).
443
   * <p>
444
   * This is called, for example, before the drag handler can be attached,
445
   * because the scrollbar for the text editor pane must be visible.
446
   * </p>
447
   *
448
   * @param node     The node to watch for showing.
449
   * @param consumer The consumer to invoke when the event fires.
450
   */
451
  private void addShowListener(
452
      final Node node, final Consumer<Void> consumer ) {
453
    final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
454
        runLater( () -> {
455
          if( newShow != null && newShow ) {
456
            try {
457
              consumer.accept( null );
458
            } catch( final Exception ex ) {
459
              alert( ex );
460
            }
461
          }
462
        } );
463
464
    Val.flatMap( node.sceneProperty(), Scene::windowProperty )
465
       .flatMap( Window::showingProperty )
466
       .addListener( listener );
467
  }
468
469
  private void scrollToParagraph( final int id ) {
470
    scrollToParagraph( id, false );
471
  }
472
473
  /**
474
   * @param id    The paragraph to scroll to, will be approximated if it doesn't
475
   *              exist.
476
   * @param force {@code true} means to force scrolling immediately, which
477
   *              should only be attempted when it is known that the document
478
   *              has been fully rendered. Otherwise the internal map of ID
479
   *              attributes will be incomplete and scrolling will flounder.
480
   */
481
  private void scrollToParagraph( final int id, final boolean force ) {
482
    synchronized( mMutex ) {
483
      final var previewPane = getPreviewPane();
484
      final var scrollPane = previewPane.getScrollPane();
485
      final int approxId = getActiveEditorPane().approximateParagraphId( id );
486
487
      if( force ) {
488
        previewPane.scrollTo( approxId );
489
      }
490
      else {
491
        previewPane.tryScrollTo( approxId );
492
      }
493
494
      scrollPane.repaint();
495
    }
496
  }
497
498
  private void updateVariableNameInjector( final FileEditorTab tab ) {
499
    getDefinitionNameInjector().addListener( tab );
500
  }
501
502
  /**
503
   * Called whenever the preview pane becomes out of sync with the file editor
504
   * tab. This can be called when the text changes, the caret paragraph
505
   * changes, or the file tab changes.
506
   *
507
   * @param tab The file editor tab that has been changed in some fashion.
508
   */
509
  private void process( final FileEditorTab tab ) {
510
    if( tab != null ) {
511
      getPreviewPane().setPath( tab.getPath() );
512
513
      final Processor<String> processor = getProcessors().computeIfAbsent(
514
          tab, p -> createProcessors( tab )
515
      );
516
517
      try {
518
        processChain( processor, tab.getEditorText() );
519
      } catch( final Exception ex ) {
520
        alert( ex );
521
      }
522
    }
523
  }
524
525
  /**
526
   * Executes the processing chain, operating on the given string.
527
   *
528
   * @param handler The first processor in the chain to call.
529
   * @param text    The initial value of the text to process.
530
   * @return The final value of the text that was processed by the chain.
531
   */
532
  private String processChain( Processor<String> handler, String text ) {
533
    while( handler != null && text != null ) {
534
      text = handler.apply( text );
535
      handler = handler.next();
536
    }
537
538
    return text;
539
  }
540
541
  private void renderActiveTab() {
542
    process( getActiveFileEditorTab() );
543
  }
544
545
  /**
546
   * Called when a definition source is opened.
547
   *
548
   * @param path Path to the definition source that was opened.
549
   */
550
  private void openDefinitions( final Path path ) {
551
    try {
552
      final var ds = createDefinitionSource( path );
553
      setDefinitionSource( ds );
554
555
      final var prefs = getUserPreferences();
556
      prefs.definitionPathProperty().setValue( path.toFile() );
557
      prefs.save();
558
559
      final var tooltipPath = new Tooltip( path.toString() );
560
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
561
562
      final var pane = getDefinitionPane();
563
      pane.update( ds );
564
      pane.addTreeChangeHandler( mTreeHandler );
565
      pane.addKeyEventHandler( mDefinitionKeyHandler );
566
      pane.filenameProperty().setValue( path.getFileName().toString() );
567
      pane.setTooltip( tooltipPath );
568
569
      interpolateResolvedMap();
570
    } catch( final Exception ex ) {
571
      alert( ex );
572
    }
573
  }
574
575
  private void exportDefinitions( final Path path ) {
576
    try {
577
      final var pane = getDefinitionPane();
578
      final var root = pane.getTreeView().getRoot();
579
      final var problemChild = pane.isTreeWellFormed();
580
581
      if( problemChild == null ) {
582
        getDefinitionSource().getTreeAdapter().export( root, path );
583
      }
584
      else {
585
        alert( "yaml.error.tree.form", problemChild.getValue() );
586
      }
587
    } catch( final Exception ex ) {
588
      alert( ex );
589
    }
590
  }
591
592
  private void interpolateResolvedMap() {
593
    final var treeMap = getDefinitionPane().toMap();
594
    final var map = new HashMap<>( treeMap );
595
    MapInterpolator.interpolate( map );
596
597
    getResolvedMap().clear();
598
    getResolvedMap().putAll( map );
599
  }
600
601
  private void initDefinitionPane() {
602
    openDefinitions( getDefinitionPath() );
603
  }
604
605
  //---- File actions -------------------------------------------------------
606
607
  /**
608
   * Called when an {@link Observable} instance has changed. This is called
609
   * by both the {@link Snitch} service and the notify service. The @link
610
   * Snitch} service can be called for different file types, including
611
   * {@link DefinitionSource} instances.
612
   *
613
   * @param observable The observed instance.
614
   * @param value      The noteworthy item.
615
   */
616
  @Override
617
  public void update( final Observable observable, final Object value ) {
618
    if( value instanceof Path && observable instanceof Snitch ) {
619
      updateSelectedTab();
620
    }
621
  }
622
623
  /**
624
   * Called when a file has been modified.
625
   */
626
  private void updateSelectedTab() {
627
    rerender();
628
  }
629
630
  /**
631
   * After resetting the processors, they will refresh anew to be up-to-date
632
   * with the files (text and definition) currently loaded into the editor.
633
   */
634
  private void resetProcessors() {
635
    getProcessors().clear();
636
  }
637
638
  //---- File actions -------------------------------------------------------
639
640
  private void fileNew() {
641
    getFileEditorPane().newEditor();
642
  }
643
644
  private void fileOpen() {
645
    getFileEditorPane().openFileDialog();
646
  }
647
648
  private void fileClose() {
649
    getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
650
  }
651
652
  /**
653
   * TODO: Upon closing, first remove the tab change listeners. (There's no
654
   * need to re-render each tab when all are being closed.)
655
   */
656
  private void fileCloseAll() {
657
    getFileEditorPane().closeAllEditors();
658
  }
659
660
  private void fileSave() {
661
    getFileEditorPane().saveEditor( getActiveFileEditorTab() );
662
  }
663
664
  private void fileSaveAs() {
665
    final FileEditorTab editor = getActiveFileEditorTab();
666
    getFileEditorPane().saveEditorAs( editor );
667
    getProcessors().remove( editor );
668
669
    try {
670
      process( editor );
671
    } catch( final Exception ex ) {
672
      alert( ex );
673
    }
674
  }
675
676
  private void fileSaveAll() {
677
    getFileEditorPane().saveAllEditors();
678
  }
679
680
  private void fileExit() {
681
    final Window window = getWindow();
682
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
683
  }
684
685
  //---- Edit actions -------------------------------------------------------
686
687
  /**
688
   * Transform the Markdown into HTML then copy that HTML into the copy
689
   * buffer.
690
   */
691
  private void copyHtml() {
692
    final var markdown = getActiveEditorPane().getText();
693
    final var processors = createProcessorFactory().createProcessors(
694
        getActiveFileEditorTab()
695
    );
696
697
    final var chain = processors.remove( HtmlPreviewProcessor.class );
698
699
    final String html = processChain( chain, markdown );
700
701
    final Clipboard clipboard = Clipboard.getSystemClipboard();
702
    final ClipboardContent content = new ClipboardContent();
703
    content.putString( html );
704
    clipboard.setContent( content );
705
  }
706
707
  /**
708
   * Used to find text in the active file editor window.
709
   */
710
  private void editFind() {
711
    final TextField input = getFindTextField();
712
    getStatusBar().setGraphic( input );
713
    input.requestFocus();
714
  }
715
716
  public void editFindNext() {
717
    getActiveFileEditorTab().searchNext( getFindTextField().getText() );
718
  }
719
720
  public void editPreferences() {
721
    getUserPreferences().show();
722
  }
723
724
  //---- Insert actions -----------------------------------------------------
725
726
  /**
727
   * Delegates to the active editor to handle wrapping the current text
728
   * selection with leading and trailing strings.
729
   *
730
   * @param leading  The string to put before the selection.
731
   * @param trailing The string to put after the selection.
732
   */
733
  private void insertMarkdown(
734
      final String leading, final String trailing ) {
735
    getActiveEditorPane().surroundSelection( leading, trailing );
736
  }
737
738
  private void insertMarkdown(
739
      final String leading, final String trailing, final String hint ) {
740
    getActiveEditorPane().surroundSelection( leading, trailing, hint );
741
  }
742
743
  //---- View actions -------------------------------------------------------
744
745
  private void viewRefresh() {
746
    rerender();
747
  }
748
749
  //---- Help actions -------------------------------------------------------
750
751
  private void helpAbout() {
752
    final Alert alert = new Alert( AlertType.INFORMATION );
753
    alert.setTitle( get( "Dialog.about.title" ) );
754
    alert.setHeaderText( get( "Dialog.about.header" ) );
755
    alert.setContentText( get( "Dialog.about.content" ) );
756
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
757
    alert.initOwner( getWindow() );
758
759
    alert.showAndWait();
760
  }
761
762
  //---- Member creators ----------------------------------------------------
763
764
  private SpellChecker createSpellChecker() {
765
    try {
766
      final Collection<String> lexicon = readLexicon( "en.txt" );
767
      return SymSpellSpeller.forLexicon( lexicon );
768
    } catch( final Exception ex ) {
769
      alert( ex );
770
      return new PermissiveSpeller();
771
    }
772
  }
773
774
  /**
775
   * Factory to create processors that are suited to different file types.
776
   *
777
   * @param tab The tab that is subjected to processing.
778
   * @return A processor suited to the file type specified by the tab's path.
779
   */
780
  private Processor<String> createProcessors( final FileEditorTab tab ) {
781
    return createProcessorFactory().createProcessors( tab );
782
  }
783
784
  private ProcessorFactory createProcessorFactory() {
785
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
786
  }
787
788
  private DefinitionPane createDefinitionPane() {
789
    return new DefinitionPane();
790
  }
791
792
  private HTMLPreviewPane createHTMLPreviewPane() {
793
    return new HTMLPreviewPane();
794
  }
795
796
  private DefinitionSource createDefaultDefinitionSource() {
797
    return new YamlDefinitionSource( getDefinitionPath() );
798
  }
799
800
  private DefinitionSource createDefinitionSource( final Path path ) {
801
    try {
802
      return createDefinitionFactory().createDefinitionSource( path );
803
    } catch( final Exception ex ) {
804
      alert( ex );
805
      return createDefaultDefinitionSource();
806
    }
807
  }
808
809
  private TextField createFindTextField() {
810
    return new TextField();
811
  }
812
813
  private DefinitionFactory createDefinitionFactory() {
814
    return new DefinitionFactory();
815
  }
816
817
  private StatusBar createStatusBar() {
818
    return new StatusBar();
819
  }
820
821
  private Scene createScene() {
822
    final SplitPane splitPane = new SplitPane(
823
        getDefinitionPane(),
824
        getFileEditorPane(),
825
        getPreviewPane() );
826
827
    splitPane.setDividerPositions(
828
        getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
829
        getFloat( K_PANE_SPLIT_EDITOR, .60f ),
830
        getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
831
832
    getDefinitionPane().prefHeightProperty()
833
                       .bind( splitPane.heightProperty() );
834
835
    final BorderPane borderPane = new BorderPane();
836
    borderPane.setPrefSize( 1280, 800 );
837
    borderPane.setTop( createMenuBar() );
838
    borderPane.setBottom( getStatusBar() );
839
    borderPane.setCenter( splitPane );
840
841
    final VBox statusBar = new VBox();
842
    statusBar.setAlignment( Pos.BASELINE_CENTER );
843
    statusBar.getChildren().add( getLineNumberText() );
844
    getStatusBar().getRightItems().add( statusBar );
845
846
    // Force preview pane refresh on Windows.
847
    if( SystemUtils.IS_OS_WINDOWS ) {
848
      splitPane.getDividers().get( 1 ).positionProperty().addListener(
849
          ( l, oValue, nValue ) -> runLater(
850
              () -> getPreviewPane().getScrollPane().repaint()
851
          )
852
      );
853
    }
854
855
    return new Scene( borderPane );
856
  }
857
858
  private Text createLineNumberText() {
859
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
860
  }
861
862
  private Node createMenuBar() {
863
    final BooleanBinding activeFileEditorIsNull =
864
        getFileEditorPane().activeFileEditorProperty().isNull();
865
866
    // File actions
867
    final Action fileNewAction = new ActionBuilder()
868
        .setText( "Main.menu.file.new" )
869
        .setAccelerator( "Shortcut+N" )
870
        .setIcon( FILE_ALT )
871
        .setAction( e -> fileNew() )
872
        .build();
873
    final Action fileOpenAction = new ActionBuilder()
874
        .setText( "Main.menu.file.open" )
875
        .setAccelerator( "Shortcut+O" )
876
        .setIcon( FOLDER_OPEN_ALT )
877
        .setAction( e -> fileOpen() )
878
        .build();
879
    final Action fileCloseAction = new ActionBuilder()
880
        .setText( "Main.menu.file.close" )
881
        .setAccelerator( "Shortcut+W" )
882
        .setAction( e -> fileClose() )
883
        .setDisable( activeFileEditorIsNull )
884
        .build();
885
    final Action fileCloseAllAction = new ActionBuilder()
886
        .setText( "Main.menu.file.close_all" )
887
        .setAction( e -> fileCloseAll() )
888
        .setDisable( activeFileEditorIsNull )
889
        .build();
890
    final Action fileSaveAction = new ActionBuilder()
891
        .setText( "Main.menu.file.save" )
892
        .setAccelerator( "Shortcut+S" )
893
        .setIcon( FLOPPY_ALT )
894
        .setAction( e -> fileSave() )
895
        .setDisable( createActiveBooleanProperty(
896
            FileEditorTab::modifiedProperty ).not() )
897
        .build();
898
    final Action fileSaveAsAction = new ActionBuilder()
899
        .setText( "Main.menu.file.save_as" )
900
        .setAction( e -> fileSaveAs() )
901
        .setDisable( activeFileEditorIsNull )
902
        .build();
903
    final Action fileSaveAllAction = new ActionBuilder()
904
        .setText( "Main.menu.file.save_all" )
905
        .setAccelerator( "Shortcut+Shift+S" )
906
        .setAction( e -> fileSaveAll() )
907
        .setDisable( Bindings.not(
908
            getFileEditorPane().anyFileEditorModifiedProperty() ) )
909
        .build();
910
    final Action fileExitAction = new ActionBuilder()
911
        .setText( "Main.menu.file.exit" )
912
        .setAction( e -> fileExit() )
913
        .build();
914
915
    // Edit actions
916
    final Action editCopyHtmlAction = new ActionBuilder()
917
        .setText( "Main.menu.edit.copy.html" )
918
        .setIcon( HTML5 )
919
        .setAction( e -> copyHtml() )
920
        .setDisable( activeFileEditorIsNull )
921
        .build();
922
923
    final Action editUndoAction = new ActionBuilder()
924
        .setText( "Main.menu.edit.undo" )
925
        .setAccelerator( "Shortcut+Z" )
926
        .setIcon( UNDO )
927
        .setAction( e -> getActiveEditorPane().undo() )
928
        .setDisable( createActiveBooleanProperty(
929
            FileEditorTab::canUndoProperty ).not() )
930
        .build();
931
    final Action editRedoAction = new ActionBuilder()
932
        .setText( "Main.menu.edit.redo" )
933
        .setAccelerator( "Shortcut+Y" )
934
        .setIcon( REPEAT )
935
        .setAction( e -> getActiveEditorPane().redo() )
936
        .setDisable( createActiveBooleanProperty(
937
            FileEditorTab::canRedoProperty ).not() )
938
        .build();
939
940
    final Action editCutAction = new ActionBuilder()
941
        .setText( "Main.menu.edit.cut" )
942
        .setAccelerator( "Shortcut+X" )
943
        .setIcon( CUT )
944
        .setAction( e -> getActiveEditorPane().cut() )
945
        .setDisable( activeFileEditorIsNull )
946
        .build();
947
    final Action editCopyAction = new ActionBuilder()
948
        .setText( "Main.menu.edit.copy" )
949
        .setAccelerator( "Shortcut+C" )
950
        .setIcon( COPY )
951
        .setAction( e -> getActiveEditorPane().copy() )
952
        .setDisable( activeFileEditorIsNull )
953
        .build();
954
    final Action editPasteAction = new ActionBuilder()
955
        .setText( "Main.menu.edit.paste" )
956
        .setAccelerator( "Shortcut+V" )
957
        .setIcon( PASTE )
958
        .setAction( e -> getActiveEditorPane().paste() )
959
        .setDisable( activeFileEditorIsNull )
960
        .build();
961
    final Action editSelectAllAction = new ActionBuilder()
962
        .setText( "Main.menu.edit.selectAll" )
963
        .setAccelerator( "Shortcut+A" )
964
        .setAction( e -> getActiveEditorPane().selectAll() )
965
        .setDisable( activeFileEditorIsNull )
966
        .build();
967
968
    final Action editFindAction = new ActionBuilder()
969
        .setText( "Main.menu.edit.find" )
970
        .setAccelerator( "Ctrl+F" )
971
        .setIcon( SEARCH )
972
        .setAction( e -> editFind() )
973
        .setDisable( activeFileEditorIsNull )
974
        .build();
975
    final Action editFindNextAction = new ActionBuilder()
976
        .setText( "Main.menu.edit.find.next" )
977
        .setAccelerator( "F3" )
978
        .setIcon( null )
979
        .setAction( e -> editFindNext() )
980
        .setDisable( activeFileEditorIsNull )
981
        .build();
982
    final Action editPreferencesAction = new ActionBuilder()
983
        .setText( "Main.menu.edit.preferences" )
984
        .setAccelerator( "Ctrl+Alt+S" )
985
        .setAction( e -> editPreferences() )
986
        .build();
987
988
    // Format actions
989
    final Action formatBoldAction = new ActionBuilder()
990
        .setText( "Main.menu.format.bold" )
991
        .setAccelerator( "Shortcut+B" )
992
        .setIcon( BOLD )
993
        .setAction( e -> insertMarkdown( "**", "**" ) )
994
        .setDisable( activeFileEditorIsNull )
995
        .build();
996
    final Action formatItalicAction = new ActionBuilder()
997
        .setText( "Main.menu.format.italic" )
998
        .setAccelerator( "Shortcut+I" )
999
        .setIcon( ITALIC )
1000
        .setAction( e -> insertMarkdown( "*", "*" ) )
1001
        .setDisable( activeFileEditorIsNull )
1002
        .build();
1003
    final Action formatSuperscriptAction = new ActionBuilder()
1004
        .setText( "Main.menu.format.superscript" )
1005
        .setAccelerator( "Shortcut+[" )
1006
        .setIcon( SUPERSCRIPT )
1007
        .setAction( e -> insertMarkdown( "^", "^" ) )
1008
        .setDisable( activeFileEditorIsNull )
1009
        .build();
1010
    final Action formatSubscriptAction = new ActionBuilder()
1011
        .setText( "Main.menu.format.subscript" )
1012
        .setAccelerator( "Shortcut+]" )
1013
        .setIcon( SUBSCRIPT )
1014
        .setAction( e -> insertMarkdown( "~", "~" ) )
1015
        .setDisable( activeFileEditorIsNull )
1016
        .build();
1017
    final Action formatStrikethroughAction = new ActionBuilder()
1018
        .setText( "Main.menu.format.strikethrough" )
1019
        .setAccelerator( "Shortcut+T" )
1020
        .setIcon( STRIKETHROUGH )
1021
        .setAction( e -> insertMarkdown( "~~", "~~" ) )
1022
        .setDisable( activeFileEditorIsNull )
1023
        .build();
1024
1025
    // Insert actions
1026
    final Action insertBlockquoteAction = new ActionBuilder()
1027
        .setText( "Main.menu.insert.blockquote" )
1028
        .setAccelerator( "Ctrl+Q" )
1029
        .setIcon( QUOTE_LEFT )
1030
        .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
1031
        .setDisable( activeFileEditorIsNull )
1032
        .build();
1033
    final Action insertCodeAction = new ActionBuilder()
1034
        .setText( "Main.menu.insert.code" )
1035
        .setAccelerator( "Shortcut+K" )
1036
        .setIcon( CODE )
1037
        .setAction( e -> insertMarkdown( "`", "`" ) )
1038
        .setDisable( activeFileEditorIsNull )
1039
        .build();
1040
    final Action insertFencedCodeBlockAction = new ActionBuilder()
1041
        .setText( "Main.menu.insert.fenced_code_block" )
1042
        .setAccelerator( "Shortcut+Shift+K" )
1043
        .setIcon( FILE_CODE_ALT )
1044
        .setAction( e -> insertMarkdown(
1045
            "\n\n```\n",
1046
            "\n```\n\n",
1047
            get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
1048
        .setDisable( activeFileEditorIsNull )
1049
        .build();
1050
    final Action insertLinkAction = new ActionBuilder()
1051
        .setText( "Main.menu.insert.link" )
1052
        .setAccelerator( "Shortcut+L" )
1053
        .setIcon( LINK )
1054
        .setAction( e -> getActiveEditorPane().insertLink() )
1055
        .setDisable( activeFileEditorIsNull )
1056
        .build();
1057
    final Action insertImageAction = new ActionBuilder()
1058
        .setText( "Main.menu.insert.image" )
1059
        .setAccelerator( "Shortcut+G" )
1060
        .setIcon( PICTURE_ALT )
1061
        .setAction( e -> getActiveEditorPane().insertImage() )
1062
        .setDisable( activeFileEditorIsNull )
1063
        .build();
1064
1065
    // Number of heading actions (H1 ... H3)
1066
    final int HEADINGS = 3;
1067
    final Action[] headings = new Action[ HEADINGS ];
1068
1069
    for( int i = 1; i <= HEADINGS; i++ ) {
1070
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
1071
      final String markup = String.format( "%n%n%s ", hashes );
1072
      final String text = "Main.menu.insert.heading." + i;
1073
      final String accelerator = "Shortcut+" + i;
1074
      final String prompt = text + ".prompt";
1075
1076
      headings[ i - 1 ] = new ActionBuilder()
1077
          .setText( text )
1078
          .setAccelerator( accelerator )
1079
          .setIcon( HEADER )
1080
          .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
1081
          .setDisable( activeFileEditorIsNull )
1082
          .build();
1083
    }
1084
1085
    final Action insertUnorderedListAction = new ActionBuilder()
1086
        .setText( "Main.menu.insert.unordered_list" )
1087
        .setAccelerator( "Shortcut+U" )
1088
        .setIcon( LIST_UL )
1089
        .setAction( e -> insertMarkdown( "\n\n* ", "" ) )
1090
        .setDisable( activeFileEditorIsNull )
1091
        .build();
1092
    final Action insertOrderedListAction = new ActionBuilder()
1093
        .setText( "Main.menu.insert.ordered_list" )
1094
        .setAccelerator( "Shortcut+Shift+O" )
1095
        .setIcon( LIST_OL )
1096
        .setAction( e -> insertMarkdown(
1097
            "\n\n1. ", "" ) )
1098
        .setDisable( activeFileEditorIsNull )
1099
        .build();
1100
    final Action insertHorizontalRuleAction = new ActionBuilder()
1101
        .setText( "Main.menu.insert.horizontal_rule" )
1102
        .setAccelerator( "Shortcut+H" )
1103
        .setAction( e -> insertMarkdown(
1104
            "\n\n---\n\n", "" ) )
1105
        .setDisable( activeFileEditorIsNull )
1106
        .build();
1107
1108
    // Definition actions
1109
    final Action definitionCreateAction = new ActionBuilder()
1110
        .setText( "Main.menu.definition.create" )
1111
        .setIcon( TREE )
1112
        .setAction( e -> getDefinitionPane().addItem() )
1113
        .build();
1114
    final Action definitionInsertAction = new ActionBuilder()
1115
        .setText( "Main.menu.definition.insert" )
1116
        .setAccelerator( "Ctrl+Space" )
1117
        .setIcon( STAR )
1118
        .setAction( e -> definitionInsert() )
1119
        .build();
1120
1121
    // Help actions
1122
    final Action helpAboutAction = new ActionBuilder()
1123
        .setText( "Main.menu.help.about" )
1124
        .setAction( e -> helpAbout() )
1125
        .build();
1126
1127
    //---- MenuBar ----
1128
1129
    // File Menu
1130
    final var fileMenu = ActionUtils.createMenu(
1131
        get( "Main.menu.file" ),
1132
        fileNewAction,
1133
        fileOpenAction,
1134
        null,
1135
        fileCloseAction,
1136
        fileCloseAllAction,
1137
        null,
1138
        fileSaveAction,
1139
        fileSaveAsAction,
1140
        fileSaveAllAction,
1141
        null,
1142
        fileExitAction );
1143
1144
    // Edit Menu
1145
    final var editMenu = ActionUtils.createMenu(
1146
        get( "Main.menu.edit" ),
1147
        editCopyHtmlAction,
1148
        null,
1149
        editUndoAction,
1150
        editRedoAction,
1151
        null,
1152
        editCutAction,
1153
        editCopyAction,
1154
        editPasteAction,
1155
        editSelectAllAction,
1156
        null,
1157
        editFindAction,
1158
        editFindNextAction,
1159
        null,
1160
        editPreferencesAction );
1161
1162
    // Format Menu
1163
    final var formatMenu = ActionUtils.createMenu(
1164
        get( "Main.menu.format" ),
1165
        formatBoldAction,
1166
        formatItalicAction,
1167
        formatSuperscriptAction,
1168
        formatSubscriptAction,
1169
        formatStrikethroughAction
1170
    );
1171
1172
    // Insert Menu
1173
    final var insertMenu = ActionUtils.createMenu(
1174
        get( "Main.menu.insert" ),
1175
        insertBlockquoteAction,
1176
        insertCodeAction,
1177
        insertFencedCodeBlockAction,
1178
        null,
1179
        insertLinkAction,
1180
        insertImageAction,
1181
        null,
1182
        headings[ 0 ],
1183
        headings[ 1 ],
1184
        headings[ 2 ],
1185
        null,
1186
        insertUnorderedListAction,
1187
        insertOrderedListAction,
1188
        insertHorizontalRuleAction
1189
    );
1190
1191
    // Definition Menu
1192
    final var definitionMenu = ActionUtils.createMenu(
1193
        get( "Main.menu.definition" ),
1194
        definitionCreateAction,
1195
        definitionInsertAction );
1196
1197
    // Help Menu
1198
    final var helpMenu = ActionUtils.createMenu(
1199
        get( "Main.menu.help" ),
1200
        helpAboutAction );
1201
1202
    //---- MenuBar ----
1203
    final var menuBar = new MenuBar(
1204
        fileMenu,
1205
        editMenu,
1206
        formatMenu,
1207
        insertMenu,
1208
        definitionMenu,
1209
        helpMenu );
1210
1211
    //---- ToolBar ----
1212
    final var toolBar = ActionUtils.createToolBar(
1213
        fileNewAction,
1214
        fileOpenAction,
1215
        fileSaveAction,
1216
        null,
1217
        editUndoAction,
1218
        editRedoAction,
1219
        editCutAction,
1220
        editCopyAction,
1221
        editPasteAction,
1222
        null,
1223
        formatBoldAction,
1224
        formatItalicAction,
1225
        formatSuperscriptAction,
1226
        formatSubscriptAction,
1227
        insertBlockquoteAction,
1228
        insertCodeAction,
1229
        insertFencedCodeBlockAction,
1230
        null,
1231
        insertLinkAction,
1232
        insertImageAction,
1233
        null,
1234
        headings[ 0 ],
1235
        null,
1236
        insertUnorderedListAction,
1237
        insertOrderedListAction );
1238
1239
    return new VBox( menuBar, toolBar );
1240
  }
1241
1242
  /**
1243
   * Performs the autoinsert function on the active file editor.
1244
   */
1245
  private void definitionInsert() {
1246
    getDefinitionNameInjector().autoinsert();
1247
  }
1248
1249
  /**
1250
   * Creates a boolean property that is bound to another boolean value of the
1251
   * active editor.
1252
   */
1253
  private BooleanProperty createActiveBooleanProperty(
1254
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1255
1256
    final BooleanProperty b = new SimpleBooleanProperty();
1257
    final FileEditorTab tab = getActiveFileEditorTab();
1258
1259
    if( tab != null ) {
1260
      b.bind( func.apply( tab ) );
1261
    }
1262
1263
    getFileEditorPane().activeFileEditorProperty().addListener(
1264
        ( observable, oldFileEditor, newFileEditor ) -> {
1265
          b.unbind();
1266
1267
          if( newFileEditor == null ) {
1268
            b.set( false );
1269
          }
1270
          else {
1271
            b.bind( func.apply( newFileEditor ) );
1272
          }
1273
        }
1274
    );
1275
1276
    return b;
1277
  }
1278
1279
  //---- Convenience accessors ----------------------------------------------
1280
1281
  private Preferences getPreferences() {
1282
    return sOptions.getState();
1283
  }
1284
1285
  private int getCurrentParagraphIndex() {
1286
    return getActiveEditorPane().getCurrentParagraphIndex();
1287
  }
1288
1289
  private float getFloat( final String key, final float defaultValue ) {
1290
    return getPreferences().getFloat( key, defaultValue );
1291
  }
1292
1293
  public Window getWindow() {
1294
    return getScene().getWindow();
1295
  }
1296
1297
  private MarkdownEditorPane getActiveEditorPane() {
1298
    return getActiveFileEditorTab().getEditorPane();
1299
  }
1300
1301
  private FileEditorTab getActiveFileEditorTab() {
1302
    return getFileEditorPane().getActiveFileEditor();
1303
  }
1304
1305
  //---- Member accessors ---------------------------------------------------
1306
1307
  protected Scene getScene() {
1308
    return mScene;
1309
  }
1310
1311
  private SpellChecker getSpellChecker() {
1312
    return mSpellChecker;
1313
  }
1314
1315
  private Map<FileEditorTab, Processor<String>> getProcessors() {
1316
    return mProcessors;
1317
  }
1318
1319
  private FileEditorTabPane getFileEditorPane() {
1320
    return mFileEditorPane;
1321
  }
1322
1323
  private HTMLPreviewPane getPreviewPane() {
1324
    return mPreviewPane;
1325
  }
1326
1327
  private void setDefinitionSource(
1328
      final DefinitionSource definitionSource ) {
1329
    assert definitionSource != null;
1330
    mDefinitionSource = definitionSource;
1331
  }
1332
1333
  private DefinitionSource getDefinitionSource() {
1334
    return mDefinitionSource;
1335
  }
1336
1337
  private DefinitionPane getDefinitionPane() {
1338
    return mDefinitionPane;
1339
  }
1340
1341
  private Text getLineNumberText() {
1342
    return mLineNumberText;
1343
  }
1344
1345
  private StatusBar getStatusBar() {
1346
    return mStatusBar;
1347
  }
1348
1349
  private TextField getFindTextField() {
1350
    return mFindTextField;
1351
  }
1352
1353
  private DefinitionNameInjector getDefinitionNameInjector() {
1354
    return mDefinitionNameInjector;
1355
  }
1356
1357
  /**
1358
   * Returns the variable map of interpolated definitions.
1359
   *
1360
   * @return A map to help dereference variables.
1361
   */
1362
  private Map<String, String> getResolvedMap() {
1363
    return mResolvedMap;
1364
  }
1365
1366
  //---- Persistence accessors ----------------------------------------------
1367
1368
  private UserPreferences getUserPreferences() {
1369
    return UserPreferences.getInstance();
1370
  }
1371
1372
  private Path getDefinitionPath() {
1373
    return getUserPreferences().getDefinitionPath();
1374
  }
1375
1376
  //---- Spelling -----------------------------------------------------------
1377
1378
  /**
1379
   * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}.
1380
   * This is called to spell check the document, rather than a single paragraph.
1381
   *
1382
   * @param text The full document text.
1383
   */
1384
  private void spellcheck(
1385
      final StyleClassedTextArea editor, final String text ) {
1386
    spellcheck( editor, text, -1 );
1387
  }
1388
1389
  /**
1390
   * Spellchecks a subset of the entire document.
1391
   *
1392
   * @param text   Look up words for this text in the lexicon.
1393
   * @param paraId Set to -1 to apply resulting style spans to the entire
1394
   *               text.
1395
   */
1396
  private void spellcheck(
1397
      final StyleClassedTextArea editor, final String text, final int paraId ) {
1398
    final var builder = new StyleSpansBuilder<Collection<String>>();
1399
    final var runningIndex = new AtomicInteger( 0 );
1400
    final var checker = getSpellChecker();
1401
1402
    // The text nodes must be relayed through a contextual "visitor" that
1403
    // can return text in chunks with correlative offsets into the string.
1404
    // This allows Markdown, R Markdown, XML, and R XML documents to return
1405
    // sets of words to check.
1406
1407
    final var node = mParser.parse( text );
1408
    final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
1409
      // Treat hyphenated compound words as individual words.
1410
      final var check = visited.replace( '-', ' ' );
1411
1412
      checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
1413
        prevIndex += bIndex;
1414
        currIndex += bIndex;
1415
1416
        // Clear styling between lexiconically absent words.
1417
        builder.add( emptyList(), prevIndex - runningIndex.get() );
1418
        builder.add( singleton( "spelling" ), currIndex - prevIndex );
1419
        runningIndex.set( currIndex );
1420
      } );
1421
    } );
1422
1423
    visitor.visit( node );
1424
1425
    // If the running index was set, at least one word triggered the listener.
1426
    if( runningIndex.get() > 0 ) {
1427
      // Clear styling after the last lexiconically absent word.
1428
      builder.add( emptyList(), text.length() - runningIndex.get() );
1429
1430
      final var spans = builder.create();
1431
1432
      if( paraId >= 0 ) {
1433
        editor.setStyleSpans( paraId, 0, spans );
1434
      }
1435
      else {
1436
        editor.setStyleSpans( 0, spans );
1437
      }
1438
    }
1439
  }
1440
1441
  @SuppressWarnings("SameParameterValue")
1442
  private Collection<String> readLexicon( final String filename )
1443
      throws Exception {
1444
    final var path = "/" + LEXICONS_DIRECTORY + "/" + filename;
1445
1446
    try( final var resource = getClass().getResourceAsStream( path ) ) {
1447
      if( resource == null ) {
1448
        throw new FileNotFoundException( path );
1449
      }
1450
1451
      try( final var isr = new InputStreamReader( resource, UTF_8 );
1452
           final var reader = new BufferedReader( isr ) ) {
1453
        return reader.lines().collect( Collectors.toList() );
1454
      }
1455
    }
1456
  }
1457
1458
  // TODO: Replace using Markdown processor instantiated for Markdown files.
1459
  // FIXME: https://github.com/DaveJarvis/scrivenvar/issues/59
1460
  private final Parser mParser = Parser.builder().build();
1461
1462
  // TODO: Replace with generic interface; provide Markdown/XML implementations.
1463
  // FIXME: https://github.com/DaveJarvis/scrivenvar/issues/59
1464
  private static final class TextVisitor {
1465
    private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
1466
        com.vladsch.flexmark.ast.Text.class, this::visit )
1467
    );
1468
1469
    private final SpellCheckListener mConsumer;
1470
1471
    public TextVisitor( final SpellCheckListener consumer ) {
1472
      mConsumer = consumer;
1473
    }
1474
1475
    private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
1476
      if( node instanceof com.vladsch.flexmark.ast.Text ) {
1477
        mConsumer.accept( node.getChars().toString(),
1478
                          node.getStartOffset(),
1479
                          node.getEndOffset() );
1480
      }
1481
1482
      mVisitor.visitChildren( node );
1483
    }
1484
  }
1485
}
14861
D src/main/java/com/scrivenvar/Messages.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  * Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  * Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar;
28
29
import java.text.MessageFormat;
30
import java.util.ResourceBundle;
31
import java.util.Stack;
32
33
import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
34
import static java.util.ResourceBundle.getBundle;
35
36
/**
37
 * Recursively resolves message properties. Property values can refer to other
38
 * properties using a <code>${var}</code> syntax.
39
 */
40
public class Messages {
41
42
  private static final ResourceBundle RESOURCE_BUNDLE =
43
      getBundle( APP_BUNDLE_NAME );
44
45
  private Messages() {
46
  }
47
48
  /**
49
   * Return the value of a resource bundle value after having resolved any
50
   * references to other bundle variables.
51
   *
52
   * @param props The bundle containing resolvable properties.
53
   * @param s     The value for a key to resolve.
54
   * @return The value of the key with all references recursively dereferenced.
55
   */
56
  @SuppressWarnings("SameParameterValue")
57
  private static String resolve( final ResourceBundle props, final String s ) {
58
    final int len = s.length();
59
    final Stack<StringBuilder> stack = new Stack<>();
60
61
    StringBuilder sb = new StringBuilder( 256 );
62
    boolean open = false;
63
64
    for( int i = 0; i < len; i++ ) {
65
      final char c = s.charAt( i );
66
67
      switch( c ) {
68
        case '$': {
69
          if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
70
            stack.push( sb );
71
            sb = new StringBuilder( 256 );
72
            i++;
73
            open = true;
74
          }
75
76
          break;
77
        }
78
79
        case '}': {
80
          if( open ) {
81
            open = false;
82
            final String name = sb.toString();
83
84
            sb = stack.pop();
85
            sb.append( props.getString( name ) );
86
            break;
87
          }
88
        }
89
90
        default: {
91
          sb.append( c );
92
          break;
93
        }
94
      }
95
    }
96
97
    if( open ) {
98
      throw new IllegalArgumentException( "missing '}'" );
99
    }
100
101
    return sb.toString();
102
  }
103
104
  /**
105
   * Returns the value for a key from the message bundle.
106
   *
107
   * @param key Retrieve the value for this key.
108
   * @return The value for the key.
109
   */
110
  public static String get( final String key ) {
111
    try {
112
      return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
113
    } catch( final Exception ex ) {
114
      return key;
115
    }
116
  }
117
118
  public static String getLiteral( final String key ) {
119
    return RESOURCE_BUNDLE.getString( key );
120
  }
121
122
  public static String get( final String key, final boolean interpolate ) {
123
    return interpolate ? get( key ) : getLiteral( key );
124
  }
125
126
  /**
127
   * Returns the value for a key from the message bundle with the arguments
128
   * replacing <code>{#}</code> place holders.
129
   *
130
   * @param key  Retrieve the value for this key.
131
   * @param args The values to substitute for place holders.
132
   * @return The value for the key.
133
   */
134
  public static String get( final String key, final Object... args ) {
135
    return MessageFormat.format( get( key ), args );
136
  }
137
}
1381
D src/main/java/com/scrivenvar/ScrollEventHandler.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import javafx.beans.property.BooleanProperty;
31
import javafx.beans.property.SimpleBooleanProperty;
32
import javafx.event.Event;
33
import javafx.event.EventHandler;
34
import javafx.scene.Node;
35
import javafx.scene.control.ScrollBar;
36
import javafx.scene.control.skin.ScrollBarSkin;
37
import javafx.scene.input.MouseEvent;
38
import javafx.scene.input.ScrollEvent;
39
import javafx.scene.layout.StackPane;
40
import org.fxmisc.flowless.VirtualizedScrollPane;
41
import org.fxmisc.richtext.StyleClassedTextArea;
42
43
import javax.swing.*;
44
45
import static javafx.geometry.Orientation.VERTICAL;
46
47
/**
48
 * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to
49
 * an instance of {@link JScrollBar}.
50
 * <p>
51
 * Called to synchronize the scrolling areas for either scrolling with the
52
 * mouse or scrolling using the scrollbar's thumb. Both are required to avoid
53
 * scrolling on the estimatedScrollYProperty that occurs when text events
54
 * fire. Scrolling performed for text events are handled separately to ensure
55
 * the preview panel scrolls to the same position in the Markdown editor,
56
 * taking into account things like images, tables, and other potentially
57
 * long vertical presentation items.
58
 * </p>
59
 */
60
public final class ScrollEventHandler implements EventHandler<Event> {
61
62
  private final class MouseHandler implements EventHandler<MouseEvent> {
63
    private final EventHandler<? super MouseEvent> mOldHandler;
64
65
    /**
66
     * Constructs a new handler for mouse scrolling events.
67
     *
68
     * @param oldHandler Receives the event after scrolling takes place.
69
     */
70
    private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) {
71
      mOldHandler = oldHandler;
72
    }
73
74
    @Override
75
    public void handle( final MouseEvent event ) {
76
      ScrollEventHandler.this.handle( event );
77
      mOldHandler.handle( event );
78
    }
79
  }
80
81
  private final class ScrollHandler implements EventHandler<ScrollEvent> {
82
    @Override
83
    public void handle( final ScrollEvent event ) {
84
      ScrollEventHandler.this.handle( event );
85
    }
86
  }
87
88
  private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane;
89
  private final JScrollBar mPreviewScrollBar;
90
  private final BooleanProperty mEnabled = new SimpleBooleanProperty();
91
92
  /**
93
   * @param editorScrollPane Scroll event source (human movement).
94
   * @param previewScrollBar Scroll event destination (corresponding movement).
95
   */
96
  public ScrollEventHandler(
97
      final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane,
98
      final JScrollBar previewScrollBar ) {
99
    mEditorScrollPane = editorScrollPane;
100
    mPreviewScrollBar = previewScrollBar;
101
102
    mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
103
104
    final var thumb = getVerticalScrollBarThumb( mEditorScrollPane );
105
    thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) );
106
  }
107
108
  /**
109
   * Gets a property intended to be bound to selected property of the tab being
110
   * scrolled. This is required because there's only one preview pane but
111
   * multiple editor panes. Each editor pane maintains its own scroll position.
112
   *
113
   * @return A {@link BooleanProperty} representing whether the scroll
114
   * events for this tab are to be executed.
115
   */
116
  public BooleanProperty enabledProperty() {
117
    return mEnabled;
118
  }
119
120
  /**
121
   * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm
122
   * is based on Karl Tauber's ratio calculation.
123
   *
124
   * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent}
125
   */
126
  @Override
127
  public void handle( final Event event ) {
128
    if( isEnabled() ) {
129
      final var eScrollPane = getEditorScrollPane();
130
      final int eScrollY =
131
          eScrollPane.estimatedScrollYProperty().getValue().intValue();
132
      final int eHeight = (int)
133
          (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
134
              - eScrollPane.getHeight());
135
      final double eRatio = eHeight > 0
136
          ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
137
138
      final var pScrollBar = getPreviewScrollBar();
139
      final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
140
      final var pScrollY = (int) (pHeight * eRatio);
141
142
      pScrollBar.setValue( pScrollY );
143
      pScrollBar.getParent().repaint();
144
    }
145
  }
146
147
  private StackPane getVerticalScrollBarThumb(
148
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
149
    final ScrollBar scrollBar = getVerticalScrollBar( pane );
150
    final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get());
151
152
    for( final Node node : skin.getChildren() ) {
153
      // Brittle, but what can you do?
154
      if( node.getStyleClass().contains( "thumb" ) ) {
155
        return (StackPane) node;
156
      }
157
    }
158
159
    throw new IllegalArgumentException( "No scroll bar skin found." );
160
  }
161
162
  private ScrollBar getVerticalScrollBar(
163
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
164
165
    for( final Node node : pane.getChildrenUnmodifiable() ) {
166
      if( node instanceof ScrollBar ) {
167
        final ScrollBar scrollBar = (ScrollBar) node;
168
169
        if( scrollBar.getOrientation() == VERTICAL ) {
170
          return scrollBar;
171
        }
172
      }
173
    }
174
175
    throw new IllegalArgumentException( "No vertical scroll pane found." );
176
  }
177
178
  private boolean isEnabled() {
179
    // TODO: As a minor optimization, when this is set to false, it could remove
180
    // the MouseHandler and ScrollHandler so that events only dispatch to one
181
    // object (instead of one per editor tab).
182
    return mEnabled.get();
183
  }
184
185
  private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() {
186
    return mEditorScrollPane;
187
  }
188
189
  private JScrollBar getPreviewScrollBar() {
190
    return mPreviewScrollBar;
191
  }
192
}
1931
D src/main/java/com/scrivenvar/Services.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
import java.util.ServiceLoader;
33
34
/**
35
 * Responsible for loading services. The services are treated as singleton
36
 * instances.
37
 */
38
public class Services {
39
40
  @SuppressWarnings("rawtypes")
41
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
42
43
  /**
44
   * Loads a service based on its interface definition. This will return an
45
   * existing instance if the class has already been instantiated.
46
   *
47
   * @param <T> The service to load.
48
   * @param api The interface definition for the service.
49
   * @return A class that implements the interface.
50
   */
51
  @SuppressWarnings("unchecked")
52
  public static <T> T load( final Class<T> api ) {
53
    final T o = (T) get( api );
54
55
    return o == null ? newInstance( api ) : o;
56
  }
57
58
  private static <T> T newInstance( final Class<T> api ) {
59
    final ServiceLoader<T> services = ServiceLoader.load( api );
60
61
    for( final T service : services ) {
62
      if( service != null ) {
63
        // Re-use the same instance the next time the class is loaded.
64
        put( api, service );
65
        return service;
66
      }
67
    }
68
69
    throw new RuntimeException( "No implementation for: " + api );
70
  }
71
72
  @SuppressWarnings("rawtypes")
73
  private static void put( final Class key, Object value ) {
74
    SINGLETONS.put( key, value );
75
  }
76
77
  @SuppressWarnings("rawtypes")
78
  private static Object get( final Class api ) {
79
    return SINGLETONS.get( api );
80
  }
81
}
821
D src/main/java/com/scrivenvar/StatusBarNotifier.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.events.Notifier;
31
import org.controlsfx.control.StatusBar;
32
33
import static com.scrivenvar.Constants.STATUS_BAR_OK;
34
import static com.scrivenvar.Messages.get;
35
import static javafx.application.Platform.runLater;
36
37
/**
38
 * Responsible for passing notifications about exceptions (or other error
39
 * messages) through the application. Once the Event Bus is implemented, this
40
 * class can go away.
41
 */
42
public class StatusBarNotifier {
43
  private static final String OK = get( STATUS_BAR_OK, "OK" );
44
45
  private static final Notifier sNotifier = Services.load( Notifier.class );
46
  private static StatusBar sStatusBar;
47
48
  public static void setStatusBar( final StatusBar statusBar ) {
49
    sStatusBar = statusBar;
50
  }
51
52
  /**
53
   * Resets the status bar to a default message.
54
   */
55
  public static void clearAlert() {
56
    // Don't burden the repaint thread if there's no status bar change.
57
    if( !OK.equals( sStatusBar.getText() ) ) {
58
      update( OK );
59
    }
60
  }
61
62
  /**
63
   * Updates the status bar with a custom message.
64
   *
65
   * @param key The resource bundle key associated with a message (typically
66
   *            to inform the user about an error).
67
   */
68
  public static void alert( final String key ) {
69
    update( get( key ) );
70
  }
71
72
  /**
73
   * Updates the status bar with a custom message.
74
   *
75
   * @param key  The property key having a value to populate with arguments.
76
   * @param args The placeholder values to substitute into the key's value.
77
   */
78
  public static void alert( final String key, final Object... args ) {
79
    update( get( key, args ) );
80
  }
81
82
  /**
83
   * Called when an exception occurs that warrants the user's attention.
84
   *
85
   * @param t The exception with a message that the user should know about.
86
   */
87
  public static void alert( final Throwable t ) {
88
    update( t.getMessage() );
89
  }
90
91
  /**
92
   * Updates the status bar to show the first line of the given message.
93
   *
94
   * @param message The message to show in the status bar.
95
   */
96
  private static void update( final String message ) {
97
    runLater(
98
        () -> {
99
          final var s = message == null ? "" : message;
100
          final var i = s.indexOf( '\n' );
101
          sStatusBar.setText( s.substring( 0, i > 0 ? i : s.length() ) );
102
        }
103
    );
104
  }
105
106
  /**
107
   * Returns the global {@link Notifier} instance that can be used for opening
108
   * pop-up alert messages.
109
   *
110
   * @return The pop-up {@link Notifier} dispatcher.
111
   */
112
  public static Notifier getNotifier() {
113
    return sNotifier;
114
  }
115
}
1161
D src/main/java/com/scrivenvar/adapters/DocumentAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.adapters;
29
30
import org.xhtmlrenderer.event.DocumentListener;
31
32
import static com.scrivenvar.StatusBarNotifier.alert;
33
34
/**
35
 * Allows subclasses to implement specific events.
36
 */
37
public class DocumentAdapter implements DocumentListener {
38
  @Override
39
  public void documentStarted() {
40
  }
41
42
  @Override
43
  public void documentLoaded() {
44
  }
45
46
  @Override
47
  public void onLayoutException( final Throwable t ) {
48
    alert( t );
49
  }
50
51
  @Override
52
  public void onRenderException( final Throwable t ) {
53
    alert( t );
54
  }
55
}
561
D src/main/java/com/scrivenvar/adapters/ReplacedElementAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.adapters;
29
30
import org.w3c.dom.Element;
31
import org.xhtmlrenderer.extend.ReplacedElementFactory;
32
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
33
34
public abstract class ReplacedElementAdapter implements ReplacedElementFactory {
35
  @Override
36
  public void reset() {
37
  }
38
39
  @Override
40
  public void remove( final Element e ) {
41
  }
42
43
  @Override
44
  public void setFormSubmissionListener(
45
      final FormSubmissionListener listener ) {
46
  }
47
}
481
D src/main/java/com/scrivenvar/controls/BrowseFileButton.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import com.scrivenvar.Messages;
31
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
32
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
33
import javafx.beans.property.ObjectProperty;
34
import javafx.beans.property.SimpleObjectProperty;
35
import javafx.event.ActionEvent;
36
import javafx.scene.control.Button;
37
import javafx.scene.control.Tooltip;
38
import javafx.scene.input.KeyCode;
39
import javafx.scene.input.KeyEvent;
40
import javafx.stage.FileChooser;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
43
import java.io.File;
44
import java.nio.file.Path;
45
import java.util.ArrayList;
46
import java.util.List;
47
48
/**
49
 * Button that opens a file chooser to select a local file for a URL.
50
 */
51
public class BrowseFileButton extends Button {
52
  private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
53
54
  public BrowseFileButton() {
55
    setGraphic(
56
        FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT )
57
    );
58
    setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
59
    setOnAction( this::browse );
60
61
    disableProperty().bind( basePath.isNull() );
62
63
    // workaround for a JavaFX bug:
64
    //   avoid closing the dialog that contains this control when the user
65
    //   closes the FileChooser or DirectoryChooser using the ESC key
66
    addEventHandler( KeyEvent.KEY_RELEASED, e -> {
67
      if( e.getCode() == KeyCode.ESCAPE ) {
68
        e.consume();
69
      }
70
    } );
71
  }
72
73
  public void addExtensionFilter( ExtensionFilter extensionFilter ) {
74
    extensionFilters.add( extensionFilter );
75
  }
76
77
  // 'basePath' property
78
  private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
79
80
  public Path getBasePath() {
81
    return basePath.get();
82
  }
83
84
  public void setBasePath( Path basePath ) {
85
    this.basePath.set( basePath );
86
  }
87
88
  // 'url' property
89
  private final ObjectProperty<String> url = new SimpleObjectProperty<>();
90
91
  public ObjectProperty<String> urlProperty() {
92
    return url;
93
  }
94
95
  protected void browse( ActionEvent e ) {
96
    FileChooser fileChooser = new FileChooser();
97
    fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
98
    fileChooser.getExtensionFilters().addAll( extensionFilters );
99
    fileChooser.getExtensionFilters()
100
               .add( new ExtensionFilter( Messages.get(
101
                   "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
102
    fileChooser.setInitialDirectory( getInitialDirectory() );
103
    File result = fileChooser.showOpenDialog( getScene().getWindow() );
104
    if( result != null ) {
105
      updateUrl( result );
106
    }
107
  }
108
109
  protected File getInitialDirectory() {
110
    //TODO build initial directory based on current value of 'url' property
111
    return getBasePath().toFile();
112
  }
113
114
  protected void updateUrl( File file ) {
115
    String newUrl;
116
    try {
117
      newUrl = getBasePath().relativize( file.toPath() ).toString();
118
    } catch( IllegalArgumentException ex ) {
119
      newUrl = file.toString();
120
    }
121
    url.set( newUrl.replace( '\\', '/' ) );
122
  }
123
}
1241
D src/main/java/com/scrivenvar/controls/EscapeTextField.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.TextField;
33
import javafx.util.StringConverter;
34
35
/**
36
 * Responsible for escaping/unescaping characters for markdown.
37
 */
38
public class EscapeTextField extends TextField {
39
40
  public EscapeTextField() {
41
    escapedText.bindBidirectional(
42
        textProperty(),
43
        new StringConverter<>() {
44
          @Override
45
          public String toString( String object ) {
46
            return escape( object );
47
          }
48
49
          @Override
50
          public String fromString( String string ) {
51
            return unescape( string );
52
          }
53
        }
54
    );
55
    escapeCharacters.addListener(
56
        e -> escapedText.set( escape( textProperty().get() ) )
57
    );
58
  }
59
60
  // 'escapedText' property
61
  private final StringProperty escapedText = new SimpleStringProperty();
62
63
  public StringProperty escapedTextProperty() {
64
    return escapedText;
65
  }
66
67
  // 'escapeCharacters' property
68
  private final StringProperty escapeCharacters = new SimpleStringProperty();
69
70
  public String getEscapeCharacters() {
71
    return escapeCharacters.get();
72
  }
73
74
  public void setEscapeCharacters( String escapeCharacters ) {
75
    this.escapeCharacters.set( escapeCharacters );
76
  }
77
78
  private String escape( final String s ) {
79
    final String escapeChars = getEscapeCharacters();
80
81
    return isEmpty( escapeChars ) ? s :
82
        s.replaceAll( "([" + escapeChars.replaceAll(
83
            "(.)",
84
            "\\\\$1" ) + "])", "\\\\$1" );
85
  }
86
87
  private String unescape( final String s ) {
88
    final String escapeChars = getEscapeCharacters();
89
90
    return isEmpty( escapeChars ) ? s :
91
        s.replaceAll( "\\\\([" + escapeChars
92
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
93
  }
94
95
  private static boolean isEmpty( final String s ) {
96
    return s == null || s.isEmpty();
97
  }
98
}
991
D src/main/java/com/scrivenvar/definition/DefinitionFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.FileType;
32
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
33
34
import java.nio.file.Path;
35
36
import static com.scrivenvar.Constants.GLOB_PREFIX_DEFINITION;
37
import static com.scrivenvar.FileType.YAML;
38
import static com.scrivenvar.util.ProtocolResolver.getProtocol;
39
40
/**
41
 * Responsible for creating objects that can read and write definition data
42
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
43
 * database.
44
 */
45
public class DefinitionFactory extends AbstractFileFactory {
46
47
  /**
48
   * Default (empty) constructor.
49
   */
50
  public DefinitionFactory() {
51
  }
52
53
  /**
54
   * Creates a definition source capable of reading definitions from the given
55
   * path.
56
   *
57
   * @param path Path to a resource containing definitions.
58
   * @return The definition source appropriate for the given path.
59
   */
60
  public DefinitionSource createDefinitionSource( final Path path ) {
61
    assert path != null;
62
63
    final var protocol = getProtocol( path.toString() );
64
    DefinitionSource result = null;
65
66
    if( protocol.isFile() ) {
67
      final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION );
68
      result = createFileDefinitionSource( filetype, path );
69
    }
70
    else {
71
      unknownFileType( protocol, path.toString() );
72
    }
73
74
    return result;
75
  }
76
77
  /**
78
   * Creates a definition source based on the file type.
79
   *
80
   * @param filetype Property key name suffix from settings.properties file.
81
   * @param path     Path to the file that corresponds to the extension.
82
   * @return A DefinitionSource capable of parsing the data stored at the path.
83
   */
84
  private DefinitionSource createFileDefinitionSource(
85
      final FileType filetype, final Path path ) {
86
    assert filetype != null;
87
    assert path != null;
88
89
    if( filetype == YAML ) {
90
      return new YamlDefinitionSource( path );
91
    }
92
93
    throw new IllegalArgumentException( filetype.toString() );
94
  }
95
}
961
D src/main/java/com/scrivenvar/definition/DefinitionPane.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
31
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
32
import javafx.beans.property.SimpleStringProperty;
33
import javafx.beans.property.StringProperty;
34
import javafx.collections.ObservableList;
35
import javafx.event.ActionEvent;
36
import javafx.event.Event;
37
import javafx.event.EventHandler;
38
import javafx.geometry.Insets;
39
import javafx.geometry.Pos;
40
import javafx.scene.Node;
41
import javafx.scene.control.*;
42
import javafx.scene.input.KeyEvent;
43
import javafx.scene.layout.BorderPane;
44
import javafx.scene.layout.HBox;
45
import javafx.util.StringConverter;
46
47
import java.util.*;
48
49
import static com.scrivenvar.Messages.get;
50
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
51
import static javafx.geometry.Pos.CENTER;
52
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
53
54
/**
55
 * Provides the user interface that holds a {@link TreeView}, which
56
 * allows users to interact with key/value pairs loaded from the
57
 * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
58
 */
59
public final class DefinitionPane extends BorderPane {
60
61
  /**
62
   * Contains a view of the definitions.
63
   */
64
  private final TreeView<String> mTreeView = new TreeView<>();
65
66
  /**
67
   * Handlers for key press events.
68
   */
69
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
70
      = new HashSet<>();
71
72
  /**
73
   * Definition file name shown in the title of the pane.
74
   */
75
  private final StringProperty mFilename = new SimpleStringProperty();
76
77
  private final TitledPane mTitledPane = new TitledPane();
78
79
  /**
80
   * Constructs a definition pane with a given tree view root.
81
   */
82
  public DefinitionPane() {
83
    final var treeView = getTreeView();
84
    treeView.setEditable( true );
85
    treeView.setCellFactory( cell -> createTreeCell() );
86
    treeView.setContextMenu( createContextMenu() );
87
    treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
88
    treeView.setShowRoot( false );
89
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
90
91
    final var bCreate = createButton(
92
        "create", TREE, e -> addItem() );
93
    final var bRename = createButton(
94
        "rename", EDIT, e -> editSelectedItem() );
95
    final var bDelete = createButton(
96
        "delete", TRASH, e -> deleteSelectedItems() );
97
98
    final var buttonBar = new HBox();
99
    buttonBar.getChildren().addAll( bCreate, bRename, bDelete );
100
    buttonBar.setAlignment( CENTER );
101
    buttonBar.setSpacing( 10 );
102
103
    final var titledPane = getTitledPane();
104
    titledPane.textProperty().bind( mFilename );
105
    titledPane.setContent( treeView );
106
    titledPane.setCollapsible( false );
107
    titledPane.setPadding( new Insets( 0, 0, 0, 0 ) );
108
109
    setTop( buttonBar );
110
    setCenter( titledPane );
111
    setAlignment( buttonBar, Pos.TOP_CENTER );
112
    setAlignment( titledPane, Pos.TOP_CENTER );
113
114
    titledPane.prefHeightProperty().bind( this.heightProperty() );
115
  }
116
117
  public void setTooltip( final Tooltip tooltip ) {
118
    getTitledPane().setTooltip( tooltip );
119
  }
120
121
  private TitledPane getTitledPane() {
122
    return mTitledPane;
123
  }
124
125
  private Button createButton(
126
      final String msgKey,
127
      final FontAwesomeIcon icon,
128
      final EventHandler<ActionEvent> eventHandler ) {
129
    final var keyPrefix = "Pane.definition.button." + msgKey;
130
    final var button = new Button( get( keyPrefix + ".label" ) );
131
    button.setOnAction( eventHandler );
132
133
    button.setGraphic(
134
        FontAwesomeIconFactory.get().createIcon( icon )
135
    );
136
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
137
138
    return button;
139
  }
140
141
  /**
142
   * Changes the root of the {@link TreeView} to the root of the
143
   * {@link TreeView} from the {@link DefinitionSource}.
144
   *
145
   * @param definitionSource Container for the hierarchy of key/value pairs
146
   *                         to replace the existing hierarchy.
147
   */
148
  public void update( final DefinitionSource definitionSource ) {
149
    assert definitionSource != null;
150
151
    final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
152
    final TreeItem<String> root = treeAdapter.adapt(
153
        get( "Pane.definition.node.root.title" )
154
    );
155
156
    getTreeView().setRoot( root );
157
  }
158
159
  public Map<String, String> toMap() {
160
    return TreeItemAdapter.toMap( getTreeView().getRoot() );
161
  }
162
163
  /**
164
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
165
   * is modified. The modifications include: item value changes, item additions,
166
   * and item removals.
167
   * <p>
168
   * Safe to call multiple times; if a handler is already registered, the
169
   * old handler is used.
170
   * </p>
171
   *
172
   * @param handler The handler to call whenever any {@link TreeItem} changes.
173
   */
174
  public void addTreeChangeHandler(
175
      final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
176
    final TreeItem<String> root = getTreeView().getRoot();
177
    root.addEventHandler( TreeItem.valueChangedEvent(), handler );
178
    root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
179
  }
180
181
  public void addKeyEventHandler(
182
      final EventHandler<? super KeyEvent> handler ) {
183
    getKeyEventHandlers().add( handler );
184
  }
185
186
  /**
187
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
188
   * well-formed for export. A tree is considered well-formed if the following
189
   * conditions are met:
190
   *
191
   * <ul>
192
   *   <li>The root node contains at least one child node having a leaf.</li>
193
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
194
   * </ul>
195
   *
196
   * @return {@code null} if the document is well-formed, otherwise the
197
   * problematic child {@link TreeItem}.
198
   */
199
  public TreeItem<String> isTreeWellFormed() {
200
    final var root = getTreeView().getRoot();
201
202
    for( final var child : root.getChildren() ) {
203
      final var problemChild = isWellFormed( child );
204
205
      if( child.isLeaf() || problemChild != null ) {
206
        return problemChild;
207
      }
208
    }
209
210
    return null;
211
  }
212
213
  /**
214
   * Determines whether the document is well-formed by ensuring that
215
   * child branches do not contain multiple leaves.
216
   *
217
   * @param item The sub-tree to check for well-formedness.
218
   * @return {@code null} when the tree is well-formed, otherwise the
219
   * problematic {@link TreeItem}.
220
   */
221
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
222
    int childLeafs = 0;
223
    int childBranches = 0;
224
225
    for( final TreeItem<String> child : item.getChildren() ) {
226
      if( child.isLeaf() ) {
227
        childLeafs++;
228
      }
229
      else {
230
        childBranches++;
231
      }
232
233
      final var problemChild = isWellFormed( child );
234
235
      if( problemChild != null ) {
236
        return problemChild;
237
      }
238
    }
239
240
    return ((childBranches > 0 && childLeafs == 0) ||
241
        (childBranches == 0 && childLeafs <= 1)) ? null : item;
242
  }
243
244
  /**
245
   * Delegates to {@link DefinitionTreeItem#findLeafExact(String)}.
246
   *
247
   * @param text The value to find, never {@code null}.
248
   * @return The leaf that contains the given value, or {@code null} if
249
   * not found.
250
   */
251
  public DefinitionTreeItem<String> findLeafExact( final String text ) {
252
    return getTreeRoot().findLeafExact( text );
253
  }
254
255
  /**
256
   * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
257
   *
258
   * @param text The value to find, never {@code null}.
259
   * @return The leaf that contains the given value, or {@code null} if
260
   * not found.
261
   */
262
  public DefinitionTreeItem<String> findLeafContains( final String text ) {
263
    return getTreeRoot().findLeafContains( text );
264
  }
265
266
  /**
267
   * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
268
   *
269
   * @param text The value to find, never {@code null}.
270
   * @return The leaf that contains the given value, or {@code null} if
271
   * not found.
272
   */
273
  public DefinitionTreeItem<String> findLeafContainsNoCase(
274
      final String text ) {
275
    return getTreeRoot().findLeafContainsNoCase( text );
276
  }
277
278
  /**
279
   * Delegates to {@link DefinitionTreeItem#findLeafStartsWith(String)}.
280
   *
281
   * @param text The value to find, never {@code null}.
282
   * @return The leaf that contains the given value, or {@code null} if
283
   * not found.
284
   */
285
  public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
286
    return getTreeRoot().findLeafStartsWith( text );
287
  }
288
289
  /**
290
   * Expands the node to the root, recursively.
291
   *
292
   * @param <T>  The type of tree item to expand (usually String).
293
   * @param node The node to expand.
294
   */
295
  public <T> void expand( final TreeItem<T> node ) {
296
    if( node != null ) {
297
      expand( node.getParent() );
298
299
      if( !node.isLeaf() ) {
300
        node.setExpanded( true );
301
      }
302
    }
303
  }
304
305
  public void select( final TreeItem<String> item ) {
306
    getSelectionModel().clearSelection();
307
    getSelectionModel().select( getTreeView().getRow( item ) );
308
  }
309
310
  /**
311
   * Collapses the tree, recursively.
312
   */
313
  public void collapse() {
314
    collapse( getTreeRoot().getChildren() );
315
  }
316
317
  /**
318
   * Collapses the tree, recursively.
319
   *
320
   * @param <T>   The type of tree item to expand (usually String).
321
   * @param nodes The nodes to collapse.
322
   */
323
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
324
    for( final var node : nodes ) {
325
      node.setExpanded( false );
326
      collapse( node.getChildren() );
327
    }
328
  }
329
330
  /**
331
   * @return {@code true} when the user is editing a {@link TreeItem}.
332
   */
333
  private boolean isEditingTreeItem() {
334
    return getTreeView().editingItemProperty().getValue() != null;
335
  }
336
337
  /**
338
   * Changes to edit mode for the selected item.
339
   */
340
  private void editSelectedItem() {
341
    getTreeView().edit( getSelectedItem() );
342
  }
343
344
  /**
345
   * Removes all selected items from the {@link TreeView}.
346
   */
347
  private void deleteSelectedItems() {
348
    for( final var item : getSelectedItems() ) {
349
      final var parent = item.getParent();
350
351
      if( parent != null ) {
352
        parent.getChildren().remove( item );
353
      }
354
    }
355
  }
356
357
  /**
358
   * Deletes the selected item.
359
   */
360
  private void deleteSelectedItem() {
361
    final var c = getSelectedItem();
362
    getSiblings( c ).remove( c );
363
  }
364
365
  /**
366
   * Adds a new item under the selected item (or root if nothing is selected).
367
   * There are a few conditions to consider: when adding to the root,
368
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
369
   * root must contain two items: a key and a value.
370
   */
371
  public void addItem() {
372
    final var value = createTreeItem();
373
    getSelectedItem().getChildren().add( value );
374
    expand( value );
375
    select( value );
376
  }
377
378
  private ContextMenu createContextMenu() {
379
    final ContextMenu menu = new ContextMenu();
380
    final ObservableList<MenuItem> items = menu.getItems();
381
382
    addMenuItem( items, "Definition.menu.create" )
383
        .setOnAction( e -> addItem() );
384
385
    addMenuItem( items, "Definition.menu.rename" )
386
        .setOnAction( e -> editSelectedItem() );
387
388
    addMenuItem( items, "Definition.menu.remove" )
389
        .setOnAction( e -> deleteSelectedItem() );
390
391
    return menu;
392
  }
393
394
  /**
395
   * Executes hot-keys for edits to the definition tree.
396
   *
397
   * @param event Contains the key code of the key that was pressed.
398
   */
399
  private void keyEventFilter( final KeyEvent event ) {
400
    if( !isEditingTreeItem() ) {
401
      switch( event.getCode() ) {
402
        case ENTER:
403
          expand( getSelectedItem() );
404
          event.consume();
405
          break;
406
407
        case DELETE:
408
          deleteSelectedItems();
409
          break;
410
411
        case INSERT:
412
          addItem();
413
          break;
414
415
        case R:
416
          if( event.isControlDown() ) {
417
            editSelectedItem();
418
          }
419
420
          break;
421
      }
422
423
      for( final var handler : getKeyEventHandlers() ) {
424
        handler.handle( event );
425
      }
426
    }
427
  }
428
429
  /**
430
   * Adds a menu item to a list of menu items.
431
   *
432
   * @param items    The list of menu items to append to.
433
   * @param labelKey The resource bundle key name for the menu item's label.
434
   * @return The menu item added to the list of menu items.
435
   */
436
  private MenuItem addMenuItem(
437
      final List<MenuItem> items, final String labelKey ) {
438
    final MenuItem menuItem = createMenuItem( labelKey );
439
    items.add( menuItem );
440
    return menuItem;
441
  }
442
443
  private MenuItem createMenuItem( final String labelKey ) {
444
    return new MenuItem( get( labelKey ) );
445
  }
446
447
  private DefinitionTreeItem<String> createTreeItem() {
448
    return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
449
  }
450
451
  private TreeCell<String> createTreeCell() {
452
    return new FocusAwareTextFieldTreeCell( createStringConverter() ) {
453
      @Override
454
      public void commitEdit( final String newValue ) {
455
        super.commitEdit( newValue );
456
        select( getTreeItem() );
457
        requestFocus();
458
      }
459
    };
460
  }
461
462
  @Override
463
  public void requestFocus() {
464
    super.requestFocus();
465
    getTreeView().requestFocus();
466
  }
467
468
  private StringConverter<String> createStringConverter() {
469
    return new StringConverter<>() {
470
      @Override
471
      public String toString( final String object ) {
472
        return object == null ? "" : object;
473
      }
474
475
      @Override
476
      public String fromString( final String string ) {
477
        return string == null ? "" : string;
478
      }
479
    };
480
  }
481
482
  /**
483
   * Returns the tree view that contains the definition hierarchy.
484
   *
485
   * @return A non-null instance.
486
   */
487
  public TreeView<String> getTreeView() {
488
    return mTreeView;
489
  }
490
491
  /**
492
   * Returns this pane.
493
   *
494
   * @return this
495
   */
496
  public Node getNode() {
497
    return this;
498
  }
499
500
  /**
501
   * Returns the property used to set the title of the pane: the file name.
502
   *
503
   * @return A non-null property used for showing the definition file name.
504
   */
505
  public StringProperty filenameProperty() {
506
    return mFilename;
507
  }
508
509
  /**
510
   * Returns the root of the tree.
511
   *
512
   * @return The first node added to the definition tree.
513
   */
514
  private DefinitionTreeItem<String> getTreeRoot() {
515
    final var root = getTreeView().getRoot();
516
517
    return root instanceof DefinitionTreeItem
518
        ? (DefinitionTreeItem<String>) root
519
        : new DefinitionTreeItem<>( "root" );
520
  }
521
522
  private ObservableList<TreeItem<String>> getSiblings(
523
      final TreeItem<String> item ) {
524
    final var root = getTreeView().getRoot();
525
    final var parent = (item == null || item == root) ? root : item.getParent();
526
527
    return parent.getChildren();
528
  }
529
530
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
531
    return getTreeView().getSelectionModel();
532
  }
533
534
  /**
535
   * Returns a copy of all the selected items.
536
   *
537
   * @return A list, possibly empty, containing all selected items in the
538
   * {@link TreeView}.
539
   */
540
  private List<TreeItem<String>> getSelectedItems() {
541
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
542
  }
543
544
  public TreeItem<String> getSelectedItem() {
545
    final var item = getSelectionModel().getSelectedItem();
546
    return item == null ? getTreeView().getRoot() : item;
547
  }
548
549
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
550
    return mKeyEventHandlers;
551
  }
552
553
  /**
554
   * Answers whether there are any definitions in the tree.
555
   *
556
   * @return {@code true} when there are no definitions; {@code false} when
557
   * there's at least one definition.
558
   */
559
  public boolean isEmpty() {
560
    return getTreeRoot().isEmpty();
561
  }
562
}
5631
D src/main/java/com/scrivenvar/definition/DefinitionSource.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
/**
31
 * Represents behaviours for reading and writing string definitions. This
32
 * class cannot have any direct hooks into the user interface, as it defines
33
 * entry points into the definition data model loaded into an object
34
 * hierarchy. That hierarchy is converted to a UI model using an adapter
35
 * pattern.
36
 */
37
public interface DefinitionSource {
38
39
  /**
40
   * Creates an object capable of producing view-based objects from this
41
   * definition source.
42
   *
43
   * @return A hierarchical tree suitable for displaying in the definition pane.
44
   */
45
  TreeAdapter getTreeAdapter();
46
}
471
D src/main/java/com/scrivenvar/definition/DefinitionTreeItem.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeItem;
31
32
import java.util.Stack;
33
import java.util.function.BiFunction;
34
35
import static java.text.Normalizer.Form.NFD;
36
import static java.text.Normalizer.normalize;
37
38
/**
39
 * Provides behaviour afforded to definition keys and corresponding value.
40
 *
41
 * @param <T> The type of {@link TreeItem} (usually string).
42
 */
43
public class DefinitionTreeItem<T> extends TreeItem<T> {
44
45
  /**
46
   * Constructs a new item with a default value.
47
   *
48
   * @param value Passed up to superclass.
49
   */
50
  public DefinitionTreeItem( final T value ) {
51
    super( value );
52
  }
53
54
  /**
55
   * Finds a leaf starting at the current node with text that matches the given
56
   * value. Search is performed case-sensitively.
57
   *
58
   * @param text The text to match against each leaf in the tree.
59
   * @return The leaf that has a value exactly matching the given text.
60
   */
61
  public DefinitionTreeItem<T> findLeafExact( final String text ) {
62
    return findLeaf( text, DefinitionTreeItem::valueEquals );
63
  }
64
65
  /**
66
   * Finds a leaf starting at the current node with text that matches the given
67
   * value. Search is performed case-sensitively.
68
   *
69
   * @param text The text to match against each leaf in the tree.
70
   * @return The leaf that has a value that contains the given text.
71
   */
72
  public DefinitionTreeItem<T> findLeafContains( final String text ) {
73
    return findLeaf( text, DefinitionTreeItem::valueContains );
74
  }
75
76
  /**
77
   * Finds a leaf starting at the current node with text that matches the given
78
   * value. Search is performed case-insensitively.
79
   *
80
   * @param text The text to match against each leaf in the tree.
81
   * @return The leaf that has a value that contains the given text.
82
   */
83
  public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
84
    return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
85
  }
86
87
  /**
88
   * Finds a leaf starting at the current node with text that matches the given
89
   * value. Search is performed case-sensitively.
90
   *
91
   * @param text The text to match against each leaf in the tree.
92
   * @return The leaf that has a value that starts with the given text.
93
   */
94
  public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
95
    return findLeaf( text, DefinitionTreeItem::valueStartsWith );
96
  }
97
98
  /**
99
   * Finds a leaf starting at the current node with text that matches the given
100
   * value.
101
   *
102
   * @param text     The text to match against each leaf in the tree.
103
   * @param findMode What algorithm is used to match the given text.
104
   * @return The leaf that has a value starting with the given text, or {@code
105
   * null} if there was no match found.
106
   */
107
  public DefinitionTreeItem<T> findLeaf(
108
      final String text,
109
      final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
110
    final var stack = new Stack<DefinitionTreeItem<T>>();
111
    stack.push( this );
112
113
    // Don't hunt for blank (empty) keys.
114
    boolean found = text.isBlank();
115
116
    while( !found && !stack.isEmpty() ) {
117
      final var node = stack.pop();
118
119
      for( final var child : node.getChildren() ) {
120
        final var result = (DefinitionTreeItem<T>) child;
121
122
        if( result.isLeaf() ) {
123
          if( found = findMode.apply( result, text ) ) {
124
            return result;
125
          }
126
        }
127
        else {
128
          stack.push( result );
129
        }
130
      }
131
    }
132
133
    return null;
134
  }
135
136
  /**
137
   * Returns the value of the string without diacritic marks.
138
   *
139
   * @return A non-null, possibly empty string.
140
   */
141
  private String getDiacriticlessValue() {
142
    return normalize( getValue().toString(), NFD )
143
        .replaceAll( "\\p{M}", "" );
144
  }
145
146
  /**
147
   * Returns true if this node is a leaf and its value equals the given text.
148
   *
149
   * @param s The text to compare against the node value.
150
   * @return true Node is a leaf and its value equals the given value.
151
   */
152
  private boolean valueEquals( final String s ) {
153
    return isLeaf() && getValue().equals( s );
154
  }
155
156
  /**
157
   * Returns true if this node is a leaf and its value contains the given text.
158
   *
159
   * @param s The text to compare against the node value.
160
   * @return true Node is a leaf and its value contains the given value.
161
   */
162
  private boolean valueContains( final String s ) {
163
    return isLeaf() && getDiacriticlessValue().contains( s );
164
  }
165
166
  /**
167
   * Returns true if this node is a leaf and its value contains the given text.
168
   *
169
   * @param s The text to compare against the node value.
170
   * @return true Node is a leaf and its value contains the given value.
171
   */
172
  private boolean valueContainsNoCase( final String s ) {
173
    return isLeaf() && getDiacriticlessValue()
174
        .toLowerCase().contains( s.toLowerCase() );
175
  }
176
177
  /**
178
   * Returns true if this node is a leaf and its value starts with the given
179
   * text.
180
   *
181
   * @param s The text to compare against the node value.
182
   * @return true Node is a leaf and its value starts with the given value.
183
   */
184
  private boolean valueStartsWith( final String s ) {
185
    return isLeaf() && getDiacriticlessValue().startsWith( s );
186
  }
187
188
  /**
189
   * Returns the path for this node, with nodes made distinct using the
190
   * separator character. This uses two loops: one for pushing nodes onto a
191
   * stack and one for popping them off to create the path in desired order.
192
   *
193
   * @return A non-null string, possibly empty.
194
   */
195
  public String toPath() {
196
    return TreeItemAdapter.toPath( getParent() );
197
  }
198
199
  /**
200
   * Answers whether there are any definitions in this tree.
201
   *
202
   * @return {@code true} when there are no definitions in the tree; {@code
203
   * false} when there is at least one definition present.
204
   */
205
  public boolean isEmpty() {
206
    return getChildren().isEmpty();
207
  }
208
}
2091
D src/main/java/com/scrivenvar/definition/DocumentParser.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
/**
31
 * Responsible for parsing structured document formats.
32
 *
33
 * @param <T> The type of "node" for the document's object model.
34
 */
35
public interface DocumentParser<T> {
36
37
  /**
38
   * Parses a document into a nested object hierarchy. The object returned
39
   * from this call must be the root node in the document tree.
40
   *
41
   * @return The document's root node, which may be empty but never null.
42
   */
43
  T getDocumentRoot();
44
}
451
D src/main/java/com/scrivenvar/definition/FocusAwareTextFieldTreeCell.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.Node;
31
import javafx.scene.control.TextField;
32
import javafx.scene.control.cell.TextFieldTreeCell;
33
import javafx.util.StringConverter;
34
35
/**
36
 * Responsible for fixing a focus lost bug in the JavaFX implementation.
37
 * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details.
38
 * This implementation borrows from the official documentation on creating
39
 * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm
40
 */
41
public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> {
42
  private TextField mTextField;
43
44
  public FocusAwareTextFieldTreeCell(
45
      final StringConverter<String> converter ) {
46
    super( converter );
47
  }
48
49
  @Override
50
  public void startEdit() {
51
    super.startEdit();
52
    var textField = mTextField;
53
54
    if( textField == null ) {
55
      textField = createTextField();
56
    }
57
    else {
58
      textField.setText( getItem() );
59
    }
60
61
    setText( null );
62
    setGraphic( textField );
63
    textField.selectAll();
64
    textField.requestFocus();
65
66
    // When the focus is lost, commit the edit then close the input field.
67
    // This fixes the unexpected behaviour when user clicks away.
68
    textField.focusedProperty().addListener( ( l, o, n ) -> {
69
      if( !n ) {
70
        commitEdit( mTextField.getText() );
71
      }
72
    } );
73
74
    mTextField = textField;
75
  }
76
77
  @Override
78
  public void cancelEdit() {
79
    super.cancelEdit();
80
    setText( getItem() );
81
    setGraphic( getTreeItem().getGraphic() );
82
  }
83
84
  @Override
85
  public void updateItem( String item, boolean empty ) {
86
    super.updateItem( item, empty );
87
88
    String text = null;
89
    Node graphic = null;
90
91
    if( !empty ) {
92
      if( isEditing() ) {
93
        final var textField = mTextField;
94
95
        if( textField != null ) {
96
          textField.setText( getString() );
97
        }
98
99
        graphic = textField;
100
      }
101
      else {
102
        text = getString();
103
        graphic = getTreeItem().getGraphic();
104
      }
105
    }
106
107
    setText( text );
108
    setGraphic( graphic );
109
  }
110
111
  private TextField createTextField() {
112
    final var textField = new TextField( getString() );
113
114
    textField.setOnKeyReleased( t -> {
115
      switch( t.getCode() ) {
116
        case ENTER -> commitEdit( textField.getText() );
117
        case ESCAPE -> cancelEdit();
118
      }
119
    } );
120
121
    return textField;
122
  }
123
124
  private String getString() {
125
    return getConverter().toString( getItem() );
126
  }
127
}
1281
D src/main/java/com/scrivenvar/definition/MapInterpolator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.sigils.YamlSigilOperator;
31
32
import java.util.Map;
33
import java.util.regex.Matcher;
34
35
import static com.scrivenvar.sigils.YamlSigilOperator.REGEX_PATTERN;
36
37
/**
38
 * Responsible for performing string interpolation on key/value pairs stored
39
 * in a map. The values in the map can use a delimited syntax to refer to
40
 * keys in the map.
41
 */
42
public class MapInterpolator {
43
  private static final int GROUP_DELIMITED = 1;
44
45
  /**
46
   * Empty.
47
   */
48
  private MapInterpolator() {
49
  }
50
51
  /**
52
   * Performs string interpolation on the values in the given map. This will
53
   * change any value in the map that contains a variable that matches
54
   * {@link YamlSigilOperator#REGEX_PATTERN}.
55
   *
56
   * @param map Contains values that represent references to keys.
57
   */
58
  public static void interpolate( final Map<String, String> map ) {
59
    map.replaceAll( ( k, v ) -> resolve( map, v ) );
60
  }
61
62
  /**
63
   * Given a value with zero or more key references, this will resolve all
64
   * the values, recursively. If a key cannot be dereferenced, the value will
65
   * contain the key name.
66
   *
67
   * @param map   Map to search for keys when resolving key references.
68
   * @param value Value containing zero or more key references
69
   * @return The given value with all embedded key references interpolated.
70
   */
71
  private static String resolve(
72
      final Map<String, String> map, String value ) {
73
    final Matcher matcher = REGEX_PATTERN.matcher( value );
74
75
    while( matcher.find() ) {
76
      final String keyName = matcher.group( GROUP_DELIMITED );
77
      final String mapValue = map.get( keyName );
78
      final String keyValue = mapValue == null
79
          ? keyName
80
          : resolve( map, mapValue );
81
82
      value = value.replace( keyName, keyValue );
83
    }
84
85
    return value;
86
  }
87
}
881
D src/main/java/com/scrivenvar/definition/RootTreeItem.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeItem;
31
import javafx.scene.control.TreeView;
32
33
/**
34
 * Indicates that this is the top-most {@link TreeItem}. This class allows
35
 * the {@link TreeItemAdapter} to ignore the topmost definition. Such
36
 * contortions are necessary because {@link TreeView} requires a root item
37
 * that isn't part of the user's definition file.
38
 * <p>
39
 * Another approach would be to associate object pairs per {@link TreeItem},
40
 * but that would be a waste of memory since the only "exception" case is
41
 * the root {@link TreeItem}.
42
 * </p>
43
 *
44
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
45
 */
46
public class RootTreeItem<T> extends DefinitionTreeItem<T> {
47
  /**
48
   * Default constructor, calls the superclass, no other behaviour.
49
   *
50
   * @param value The {@link TreeItem} node name to construct the superclass.
51
   * @see TreeItemAdapter#toMap(TreeItem) for details on how this
52
   * class is used.
53
   */
54
  public RootTreeItem( final T value ) {
55
    super( value );
56
  }
57
}
581
D src/main/java/com/scrivenvar/definition/TreeAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeItem;
31
32
import java.io.IOException;
33
import java.nio.file.Path;
34
35
/**
36
 * Responsible for converting an object hierarchy into a {@link TreeItem}
37
 * hierarchy.
38
 */
39
public interface TreeAdapter {
40
  /**
41
   * Adapts the document produced by the given parser into a {@link TreeItem}
42
   * object that can be presented to the user within a GUI.
43
   *
44
   * @param root The default root node name.
45
   * @return The parsed document in a {@link TreeItem} that can be displayed
46
   * in a panel.
47
   */
48
  TreeItem<String> adapt( String root );
49
50
  /**
51
   * Exports the given root node to the given path.
52
   *
53
   * @param root The root node to export.
54
   * @param path Where to persist the data.
55
   * @throws IOException Could not write the data to the given path.
56
   */
57
  void export( TreeItem<String> root, Path path ) throws IOException;
58
}
591
D src/main/java/com/scrivenvar/definition/TreeItemAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.scrivenvar.sigils.YamlSigilOperator;
32
import com.scrivenvar.preview.HTMLPreviewPane;
33
import javafx.scene.control.TreeItem;
34
import javafx.scene.control.TreeView;
35
36
import java.util.HashMap;
37
import java.util.Iterator;
38
import java.util.Map;
39
import java.util.Stack;
40
41
import static com.scrivenvar.Constants.DEFAULT_MAP_SIZE;
42
43
/**
44
 * Given a {@link TreeItem}, this will generate a flat map with all the
45
 * values in the tree recursively interpolated. The application integrates
46
 * definition files as follows:
47
 * <ol>
48
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
49
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
50
 *   <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
51
 *   <li>Substitute flat map variables into document as required.</li>
52
 * </ol>
53
 *
54
 * <p>
55
 * This class is responsible for producing the interpolated flat map. This
56
 * allows dynamic edits of the {@link TreeView} to be displayed in the
57
 * {@link HTMLPreviewPane} without having to reload the definition file.
58
 * Reloading the definition file would work, but has a number of drawbacks.
59
 * </p>
60
 */
61
public class TreeItemAdapter {
62
  /**
63
   * Separates YAML definition keys (e.g., the dots in {@code $root.node.var$}).
64
   */
65
  public static final String SEPARATOR = ".";
66
67
  /**
68
   * Default buffer length for keys ({@link StringBuilder} has 16 character
69
   * buffer) that should be large enough for most keys to avoid reallocating
70
   * memory to increase the {@link StringBuilder}'s buffer.
71
   */
72
  public static final int DEFAULT_KEY_LENGTH = 64;
73
74
  /**
75
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
76
   * as a consecutive list.
77
   */
78
  private static final class TreeIterator
79
      implements Iterator<TreeItem<String>> {
80
    private final Stack<TreeItem<String>> mStack = new Stack<>();
81
82
    public TreeIterator( final TreeItem<String> root ) {
83
      if( root != null ) {
84
        mStack.push( root );
85
      }
86
    }
87
88
    @Override
89
    public boolean hasNext() {
90
      return !mStack.isEmpty();
91
    }
92
93
    @Override
94
    public TreeItem<String> next() {
95
      final TreeItem<String> next = mStack.pop();
96
      next.getChildren().forEach( mStack::push );
97
98
      return next;
99
    }
100
  }
101
102
  private TreeItemAdapter() {
103
  }
104
105
  /**
106
   * Iterate over a given root node (at any level of the tree) and process each
107
   * leaf node into a flat map. Values must be interpolated separately.
108
   */
109
  public static Map<String, String> toMap( final TreeItem<String> root ) {
110
    final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
111
    final TreeIterator iterator = new TreeIterator( root );
112
113
    iterator.forEachRemaining( item -> {
114
      if( item.isLeaf() ) {
115
        map.put( toPath( item.getParent() ), item.getValue() );
116
      }
117
    } );
118
119
    return map;
120
  }
121
122
123
  /**
124
   * For a given node, this will ascend the tree to generate a key name
125
   * that is associated with the leaf node's value.
126
   *
127
   * @param node Ascendants represent the key to this node's value.
128
   * @param <T>  Data type that the {@link TreeItem} contains.
129
   * @return The string representation of the node's unique key.
130
   */
131
  public static <T> String toPath( TreeItem<T> node ) {
132
    assert node != null;
133
134
    final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH );
135
    final Stack<TreeItem<T>> stack = new Stack<>();
136
137
    while( node != null && !(node instanceof RootTreeItem) ) {
138
      stack.push( node );
139
      node = node.getParent();
140
    }
141
142
    // Gets set at end of first iteration (to avoid an if condition).
143
    String separator = "";
144
145
    while( !stack.empty() ) {
146
      final T subkey = stack.pop().getValue();
147
      key.append( separator );
148
      key.append( subkey );
149
      separator = SEPARATOR;
150
    }
151
152
    return YamlSigilOperator.entoken( key.toString() );
153
  }
154
}
1551
D src/main/java/com/scrivenvar/definition/yaml/YamlDefinitionSource.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.scrivenvar.definition.DefinitionSource;
31
import com.scrivenvar.definition.TreeAdapter;
32
33
import java.nio.file.Path;
34
35
/**
36
 * Represents a definition data source for YAML files.
37
 */
38
public class YamlDefinitionSource implements DefinitionSource {
39
40
  private final YamlTreeAdapter mYamlTreeAdapter;
41
42
  /**
43
   * Constructs a new YAML definition source, populated from the given file.
44
   *
45
   * @param path Path to the YAML definition file.
46
   */
47
  public YamlDefinitionSource( final Path path ) {
48
    assert path != null;
49
50
    mYamlTreeAdapter = new YamlTreeAdapter( path );
51
  }
52
53
  @Override
54
  public TreeAdapter getTreeAdapter() {
55
    return mYamlTreeAdapter;
56
  }
57
}
581
D src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.ObjectMapper;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
33
import com.scrivenvar.definition.DocumentParser;
34
35
import java.io.InputStream;
36
import java.nio.file.Files;
37
import java.nio.file.Path;
38
39
/**
40
 * Responsible for reading a YAML document into an object hierarchy.
41
 */
42
public class YamlParser implements DocumentParser<JsonNode> {
43
44
  /**
45
   * Start of the Universe (the YAML document node that contains all others).
46
   */
47
  private final JsonNode mDocumentRoot;
48
49
  /**
50
   * Creates a new YamlParser instance that attempts to parse the contents
51
   * of the YAML document given from a path. In the event that the file either
52
   * does not exist or is empty, a fake
53
   *
54
   * @param path Path to a file containing YAML data to parse.
55
   */
56
  public YamlParser( final Path path ) {
57
    assert path != null;
58
    mDocumentRoot = parse( path );
59
  }
60
61
  /**
62
   * Returns the parent node for the entire YAML document tree.
63
   *
64
   * @return The document root, never {@code null}.
65
   */
66
  @Override
67
  public JsonNode getDocumentRoot() {
68
    return mDocumentRoot;
69
  }
70
71
  /**
72
   * Parses the given path containing YAML data into an object hierarchy.
73
   *
74
   * @param path {@link Path} to the YAML resource to parse.
75
   * @return The parsed contents, or an empty object hierarchy.
76
   */
77
  private JsonNode parse( final Path path ) {
78
    try( final InputStream in = Files.newInputStream( path ) ) {
79
      return new ObjectMapper( new YAMLFactory() ).readTree( in );
80
    } catch( final Exception e ) {
81
      // Ensure that a document root node exists by relying on the
82
      // default failure condition when processing. This is required
83
      // because the input stream could not be read.
84
      return new ObjectMapper().createObjectNode();
85
    }
86
  }
87
}
881
D src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.node.ObjectNode;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
33
import com.scrivenvar.definition.RootTreeItem;
34
import com.scrivenvar.definition.TreeAdapter;
35
import com.scrivenvar.definition.DefinitionTreeItem;
36
import javafx.scene.control.TreeItem;
37
import javafx.scene.control.TreeView;
38
39
import java.io.IOException;
40
import java.nio.file.Path;
41
import java.util.Map.Entry;
42
43
/**
44
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
45
 * interface and vice-versa.
46
 */
47
public class YamlTreeAdapter implements TreeAdapter {
48
  private final YamlParser mParser;
49
50
  /**
51
   * Constructs a new instance that will use the given path to read
52
   * the object hierarchy from a data source.
53
   *
54
   * @param path Path to YAML contents to parse.
55
   */
56
  public YamlTreeAdapter( final Path path ) {
57
    mParser = new YamlParser( path );
58
  }
59
60
  @Override
61
  public void export( final TreeItem<String> treeItem, final Path path )
62
      throws IOException {
63
    final YAMLMapper mapper = new YAMLMapper();
64
    final ObjectNode root = mapper.createObjectNode();
65
66
    // Iterate over the root item's children. The root item is used by the
67
    // application to ensure definitions can always be added to a tree, as
68
    // such it is not meant to be exported, only its children.
69
    for( final TreeItem<String> child : treeItem.getChildren() ) {
70
      export( child, root );
71
    }
72
73
    // Writes as UTF8 by default.
74
    mapper.writeValue( path.toFile(), root );
75
  }
76
77
  /**
78
   * Recursive method to generate an object hierarchy that represents the
79
   * given {@link TreeItem} hierarchy.
80
   *
81
   * @param item The {@link TreeItem} to reproduce as an object hierarchy.
82
   * @param node The {@link ObjectNode} to update to reflect the
83
   *             {@link TreeItem} hierarchy.
84
   */
85
  private void export( final TreeItem<String> item, ObjectNode node ) {
86
    final var children = item.getChildren();
87
88
    // If the current item has more than one non-leaf child, it's an
89
    // object node and must become a new nested object.
90
    if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
91
      node = node.putObject( item.getValue() );
92
    }
93
94
    for( final TreeItem<String> child : children ) {
95
      if( child.isLeaf() ) {
96
        node.put( item.getValue(), child.getValue() );
97
      }
98
      else {
99
        export( child, node );
100
      }
101
    }
102
  }
103
104
  /**
105
   * Converts a YAML document to a {@link TreeItem} based on the document
106
   * keys. Only the first document in the stream is adapted.
107
   *
108
   * @param root Root {@link TreeItem} node name.
109
   * @return A {@link TreeItem} populated with all the keys in the YAML
110
   * document.
111
   */
112
  public TreeItem<String> adapt( final String root ) {
113
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
114
    final TreeItem<String> rootItem = createRootTreeItem( root );
115
116
    rootItem.setExpanded( true );
117
    adapt( rootNode, rootItem );
118
    return rootItem;
119
  }
120
121
  /**
122
   * Iterate over a given root node (at any level of the tree) and adapt each
123
   * leaf node.
124
   *
125
   * @param rootNode A JSON node (YAML node) to adapt.
126
   * @param rootItem The tree item to use as the root when processing the node.
127
   */
128
  private void adapt(
129
      final JsonNode rootNode, final TreeItem<String> rootItem ) {
130
    rootNode.fields().forEachRemaining(
131
        ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
132
    );
133
  }
134
135
  /**
136
   * Recursively adapt each rootNode to a corresponding rootItem.
137
   *
138
   * @param rootNode The node to adapt.
139
   * @param rootItem The item to adapt using the node's key.
140
   */
141
  private void adapt(
142
      final Entry<String, JsonNode> rootNode,
143
      final TreeItem<String> rootItem ) {
144
    final JsonNode leafNode = rootNode.getValue();
145
    final String key = rootNode.getKey();
146
    final TreeItem<String> leaf = createTreeItem( key );
147
148
    if( leafNode.isValueNode() ) {
149
      leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
150
    }
151
152
    rootItem.getChildren().add( leaf );
153
154
    if( leafNode.isObject() ) {
155
      adapt( leafNode, leaf );
156
    }
157
  }
158
159
  /**
160
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
161
   *
162
   * @param value The node's value.
163
   * @return A new {@link TreeItem}, never {@code null}.
164
   */
165
  private TreeItem<String> createTreeItem( final String value ) {
166
    return new DefinitionTreeItem<>( value );
167
  }
168
169
  /**
170
   * Creates a new {@link TreeItem} that is intended to be the root-level item
171
   * added to the {@link TreeView}. This allows the root item to be
172
   * distinguished from the other items so that reference keys do not include
173
   * "Definition" as part of their name.
174
   *
175
   * @param value The node's value.
176
   * @return A new {@link TreeItem}, never {@code null}.
177
   */
178
  private TreeItem<String> createRootTreeItem( final String value ) {
179
    return new RootTreeItem<>( value );
180
  }
181
182
  public YamlParser getYamlParser() {
183
    return mParser;
184
  }
185
}
1861
D src/main/java/com/scrivenvar/dialogs/AbstractDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.service.events.impl.ButtonOrderPane;
32
import static javafx.scene.control.ButtonType.CANCEL;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
37
/**
38
 * Superclass that abstracts common behaviours for all dialogs.
39
 *
40
 * @param <T> The type of dialog to create (usually String).
41
 */
42
public abstract class AbstractDialog<T> extends Dialog<T> {
43
44
  /**
45
   * Ensures that all dialogs can be closed.
46
   *
47
   * @param owner The parent window of this dialog.
48
   * @param title The messages title to display in the title bar.
49
   */
50
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
51
  public AbstractDialog( final Window owner, final String title ) {
52
    setTitle( get( title ) );
53
    setResizable( true );
54
55
    initOwner( owner );
56
    initCloseAction();
57
    initDialogPane();
58
    initDialogButtons();
59
    initComponents();
60
  }
61
62
  /**
63
   * Initialize the component layout.
64
   */
65
  protected abstract void initComponents();
66
67
  /**
68
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
69
   */
70
  protected void initDialogPane() {
71
    setDialogPane( new ButtonOrderPane() );
72
  }
73
  
74
  /**
75
   * Set an OK and CANCEL button on the dialog.
76
   */
77
  protected void initDialogButtons() {
78
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
79
  }
80
81
  /**
82
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
83
   * can always close the window, even if there's an error.
84
   */
85
  protected final void initCloseAction() {
86
    final Window window = getDialogPane().getScene().getWindow();
87
    window.setOnCloseRequest( event -> window.hide() );
88
  }
89
}
901
D src/main/java/com/scrivenvar/dialogs/ImageDialog.java
1
/*
2
 * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.dialogs;
28
29
import static com.scrivenvar.Messages.get;
30
import com.scrivenvar.controls.BrowseFileButton;
31
import com.scrivenvar.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a markdown image.
47
 */
48
public class ImageDialog extends AbstractDialog<String> {
49
50
  private final StringProperty image = new SimpleStringProperty();
51
52
  public ImageDialog( final Window owner, final Path basePath ) {
53
    super(owner, "Dialog.image.title" );
54
    
55
    final DialogPane dialogPane = getDialogPane();
56
    dialogPane.setContent( pane );
57
58
    linkBrowseFileButton.setBasePath( basePath );
59
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
60
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
61
62
    dialogPane.lookupButton( OK ).disableProperty().bind(
63
      urlField.escapedTextProperty().isEmpty()
64
      .or( textField.escapedTextProperty().isEmpty() ) );
65
66
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
69
    previewField.textProperty().bind( image );
70
71
    setResultConverter( dialogButton -> {
72
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
73
      return (data == ButtonData.OK_DONE) ? image.get() : null;
74
    } );
75
76
    Platform.runLater( () -> {
77
      urlField.requestFocus();
78
79
      if( urlField.getText().startsWith( "http://" ) ) {
80
        urlField.selectRange( "http://".length(), urlField.getLength() );
81
      }
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    linkBrowseFileButton = new BrowseFileButton();
92
    Label textLabel = new Label();
93
    textField = new EscapeTextField();
94
    Label titleLabel = new Label();
95
    titleField = new EscapeTextField();
96
    Label previewLabel = new Label();
97
    previewField = new Label();
98
99
    //======== pane ========
100
    {
101
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
102
      pane.setRows( "[][][][]" );
103
104
      //---- urlLabel ----
105
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
106
      pane.add( urlLabel, "cell 0 0" );
107
108
      //---- urlField ----
109
      urlField.setEscapeCharacters( "()" );
110
      urlField.setText( "http://yourlink.com" );
111
      urlField.setPromptText( "http://yourlink.com" );
112
      pane.add( urlField, "cell 1 0" );
113
      pane.add( linkBrowseFileButton, "cell 2 0" );
114
115
      //---- textLabel ----
116
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
117
      pane.add( textLabel, "cell 0 1" );
118
119
      //---- textField ----
120
      textField.setEscapeCharacters( "[]" );
121
      pane.add( textField, "cell 1 1 2 1" );
122
123
      //---- titleLabel ----
124
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
125
      pane.add( titleLabel, "cell 0 2" );
126
      pane.add( titleField, "cell 1 2 2 1" );
127
128
      //---- previewLabel ----
129
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
130
      pane.add( previewLabel, "cell 0 3" );
131
      pane.add( previewField, "cell 1 3 2 1" );
132
    }
133
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
134
  }
135
136
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
137
  private MigPane pane;
138
  private EscapeTextField urlField;
139
  private BrowseFileButton linkBrowseFileButton;
140
  private EscapeTextField textField;
141
  private EscapeTextField titleField;
142
  private Label previewField;
143
	// JFormDesigner - End of variables declaration  //GEN-END:variables
144
}
1451
D src/main/java/com/scrivenvar/dialogs/ImageDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "ImageDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "ImageDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
34
				name: "linkBrowseFileButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "javafx.scene.control.Label" ) {
39
				name: "textLabel"
40
				"text": new FormMessage( null, "ImageDialog.textLabel.text" )
41
				auxiliary() {
42
					"JavaCodeGenerator.variableLocal": true
43
				}
44
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
45
				"value": "cell 0 1"
46
			} )
47
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
48
				name: "textField"
49
				"escapeCharacters": "[]"
50
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
51
				"value": "cell 1 1 2 1"
52
			} )
53
			add( new FormComponent( "javafx.scene.control.Label" ) {
54
				name: "titleLabel"
55
				"text": new FormMessage( null, "ImageDialog.titleLabel.text" )
56
				auxiliary() {
57
					"JavaCodeGenerator.variableLocal": true
58
				}
59
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
60
				"value": "cell 0 2"
61
			} )
62
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
63
				name: "titleField"
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 1 2 2 1"
66
			} )
67
			add( new FormComponent( "javafx.scene.control.Label" ) {
68
				name: "previewLabel"
69
				"text": new FormMessage( null, "ImageDialog.previewLabel.text" )
70
				auxiliary() {
71
					"JavaCodeGenerator.variableLocal": true
72
				}
73
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
74
				"value": "cell 0 3"
75
			} )
76
			add( new FormComponent( "javafx.scene.control.Label" ) {
77
				name: "previewField"
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 1 3 2 1"
80
			} )
81
		}, new FormLayoutConstraints( null ) {
82
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
83
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
84
		} )
85
	}
86
}
871
D src/main/java/com/scrivenvar/dialogs/LinkDialog.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import com.scrivenvar.controls.EscapeTextField;
31
import com.scrivenvar.editors.markdown.HyperlinkModel;
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.SimpleStringProperty;
35
import javafx.beans.property.StringProperty;
36
import javafx.scene.control.ButtonBar.ButtonData;
37
import javafx.scene.control.DialogPane;
38
import javafx.scene.control.Label;
39
import javafx.stage.Window;
40
import org.tbee.javafx.scene.layout.fxml.MigPane;
41
42
import static com.scrivenvar.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
44
45
/**
46
 * Dialog to enter a markdown link.
47
 */
48
public class LinkDialog extends AbstractDialog<String> {
49
50
  private final StringProperty link = new SimpleStringProperty();
51
52
  public LinkDialog(
53
    final Window owner, final HyperlinkModel hyperlink ) {
54
    super( owner, "Dialog.link.title" );
55
56
    final DialogPane dialogPane = getDialogPane();
57
    dialogPane.setContent( pane );
58
59
    dialogPane.lookupButton( OK ).disableProperty().bind(
60
      urlField.escapedTextProperty().isEmpty() );
61
62
    textField.setText( hyperlink.getText() );
63
    urlField.setText( hyperlink.getUrl() );
64
    titleField.setText( hyperlink.getTitle() );
65
66
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
69
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
70
        .otherwise( urlField.escapedTextProperty() ) ) );
71
72
    setResultConverter( dialogButton -> {
73
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
74
      return (data == ButtonData.OK_DONE) ? link.get() : null;
75
    } );
76
77
    Platform.runLater( () -> {
78
      urlField.requestFocus();
79
      urlField.selectRange( 0, urlField.getLength() );
80
    } );
81
  }
82
83
  @Override
84
  protected void initComponents() {
85
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
86
    pane = new MigPane();
87
    Label urlLabel = new Label();
88
    urlField = new EscapeTextField();
89
    Label textLabel = new Label();
90
    textField = new EscapeTextField();
91
    Label titleLabel = new Label();
92
    titleField = new EscapeTextField();
93
94
    //======== pane ========
95
    {
96
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
97
      pane.setRows( "[][][][]" );
98
99
      //---- urlLabel ----
100
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
101
      pane.add( urlLabel, "cell 0 0" );
102
103
      //---- urlField ----
104
      urlField.setEscapeCharacters( "()" );
105
      pane.add( urlField, "cell 1 0" );
106
107
      //---- textLabel ----
108
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
109
      pane.add( textLabel, "cell 0 1" );
110
111
      //---- textField ----
112
      textField.setEscapeCharacters( "[]" );
113
      pane.add( textField, "cell 1 1 3 1" );
114
115
      //---- titleLabel ----
116
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
117
      pane.add( titleLabel, "cell 0 2" );
118
      pane.add( titleField, "cell 1 2 3 1" );
119
    }
120
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
121
  }
122
123
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
124
  private MigPane pane;
125
  private EscapeTextField urlField;
126
  private EscapeTextField textField;
127
  private EscapeTextField titleField;
128
  // JFormDesigner - End of variables declaration  //GEN-END:variables
129
}
1301
D src/main/java/com/scrivenvar/dialogs/LinkDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "LinkDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "LinkDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
34
				name: "linkBrowseDirectoyButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
39
				name: "linkBrowseFileButton"
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 3 0"
42
			} )
43
			add( new FormComponent( "javafx.scene.control.Label" ) {
44
				name: "textLabel"
45
				"text": new FormMessage( null, "LinkDialog.textLabel.text" )
46
				auxiliary() {
47
					"JavaCodeGenerator.variableLocal": true
48
				}
49
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
50
				"value": "cell 0 1"
51
			} )
52
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
53
				name: "textField"
54
				"escapeCharacters": "[]"
55
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
56
				"value": "cell 1 1 3 1"
57
			} )
58
			add( new FormComponent( "javafx.scene.control.Label" ) {
59
				name: "titleLabel"
60
				"text": new FormMessage( null, "LinkDialog.titleLabel.text" )
61
				auxiliary() {
62
					"JavaCodeGenerator.variableLocal": true
63
				}
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 0 2"
66
			} )
67
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
68
				name: "titleField"
69
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
70
				"value": "cell 1 2 3 1"
71
			} )
72
			add( new FormComponent( "javafx.scene.control.Label" ) {
73
				name: "previewLabel"
74
				"text": new FormMessage( null, "LinkDialog.previewLabel.text" )
75
				auxiliary() {
76
					"JavaCodeGenerator.variableLocal": true
77
				}
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 0 3"
80
			} )
81
			add( new FormComponent( "javafx.scene.control.Label" ) {
82
				name: "previewField"
83
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
84
				"value": "cell 1 3 3 1"
85
			} )
86
		}, new FormLayoutConstraints( null ) {
87
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
88
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
89
		} )
90
	}
91
}
921
D src/main/java/com/scrivenvar/editors/DefinitionDecoratorFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.sigils.RSigilOperator;
32
import com.scrivenvar.sigils.SigilOperator;
33
import com.scrivenvar.sigils.YamlSigilOperator;
34
35
import java.nio.file.Path;
36
37
/**
38
 * Responsible for creating a definition name decorator suited to a particular
39
 * file type.
40
 */
41
public class DefinitionDecoratorFactory extends AbstractFileFactory {
42
43
  private DefinitionDecoratorFactory() {
44
  }
45
46
  public static SigilOperator newInstance( final Path path ) {
47
    final var factory = new DefinitionDecoratorFactory();
48
49
    return switch( factory.lookup( path ) ) {
50
      case RMARKDOWN, RXML -> new RSigilOperator();
51
      default -> new YamlSigilOperator();
52
    };
53
  }
54
}
551
D src/main/java/com/scrivenvar/editors/DefinitionNameInjector.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.FileEditorTab;
31
import com.scrivenvar.definition.DefinitionPane;
32
import com.scrivenvar.definition.DefinitionTreeItem;
33
import com.scrivenvar.sigils.SigilOperator;
34
import javafx.scene.control.TreeItem;
35
import javafx.scene.input.KeyEvent;
36
import org.fxmisc.richtext.StyledTextArea;
37
38
import java.nio.file.Path;
39
import java.text.BreakIterator;
40
41
import static com.scrivenvar.Constants.*;
42
import static com.scrivenvar.StatusBarNotifier.alert;
43
import static java.lang.Character.isWhitespace;
44
import static javafx.scene.input.KeyCode.SPACE;
45
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
46
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
47
48
/**
49
 * Provides the logic for injecting variable names within the editor.
50
 */
51
public final class DefinitionNameInjector {
52
53
  /**
54
   * Recipient of name injections.
55
   */
56
  private FileEditorTab mTab;
57
58
  /**
59
   * Initiates double-click events.
60
   */
61
  private final DefinitionPane mDefinitionPane;
62
63
  /**
64
   * Initializes the variable name injector against the given pane.
65
   *
66
   * @param pane The definition panel to listen to for double-click events.
67
   */
68
  public DefinitionNameInjector( final DefinitionPane pane ) {
69
    mDefinitionPane = pane;
70
  }
71
72
  /**
73
   * Trap Control+Space.
74
   *
75
   * @param tab Editor where variable names get injected.
76
   */
77
  public void addListener( final FileEditorTab tab ) {
78
    assert tab != null;
79
    mTab = tab;
80
81
    tab.getEditorPane().addKeyboardListener(
82
        keyPressed( SPACE, CONTROL_DOWN ),
83
        this::autoinsert
84
    );
85
  }
86
87
  /**
88
   * Inserts the currently selected variable from the {@link DefinitionPane}.
89
   */
90
  public void injectSelectedItem() {
91
    final var pane = getDefinitionPane();
92
    final TreeItem<String> item = pane.getSelectedItem();
93
94
    if( item.isLeaf() ) {
95
      final var leaf = pane.findLeafExact( item.getValue() );
96
      final var editor = getEditor();
97
98
      editor.insertText( editor.getCaretPosition(), decorate( leaf ) );
99
    }
100
  }
101
102
  /**
103
   * Pressing Control+SPACE will find a node that matches the current word and
104
   * substitute the definition reference.
105
   */
106
  public void autoinsert() {
107
    final String paragraph = getCaretParagraph();
108
    final int[] bounds = getWordBoundariesAtCaret();
109
110
    try {
111
      if( isEmptyDefinitionPane() ) {
112
        alert( STATUS_DEFINITION_EMPTY );
113
      }
114
      else {
115
        final String word = paragraph.substring( bounds[ 0 ], bounds[ 1 ] );
116
117
        if( word.isBlank() ) {
118
          alert( STATUS_DEFINITION_BLANK );
119
        }
120
        else {
121
          final var leaf = findLeaf( word );
122
123
          if( leaf == null ) {
124
            alert( STATUS_DEFINITION_MISSING, word );
125
          }
126
          else {
127
            replaceText( bounds[ 0 ], bounds[ 1 ], decorate( leaf ) );
128
            expand( leaf );
129
          }
130
        }
131
      }
132
    } catch( final Exception ignored ) {
133
      alert( STATUS_DEFINITION_BLANK );
134
    }
135
  }
136
137
  /**
138
   * Pressing Control+SPACE will find a node that matches the current word and
139
   * substitute the definition reference.
140
   *
141
   * @param e Ignored -- it can only be Control+SPACE.
142
   */
143
  private void autoinsert( final KeyEvent e ) {
144
    autoinsert();
145
  }
146
147
  /**
148
   * Finds the start and end indexes for the word in the current paragraph
149
   * where the caret is located. There are a few different scenarios, where
150
   * the caret can be at: the start, end, or middle of a word; also, the
151
   * caret can be at the end or beginning of a punctuated word; as well, the
152
   * caret could be at the beginning or end of the line or document.
153
   */
154
  private int[] getWordBoundariesAtCaret() {
155
    final var paragraph = getCaretParagraph();
156
    final var length = paragraph.length();
157
    int offset = getCurrentCaretColumn();
158
159
    int began = offset;
160
    int ended = offset;
161
162
    while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
163
      began--;
164
    }
165
166
    while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
167
      ended++;
168
    }
169
170
    final var iterator = BreakIterator.getWordInstance();
171
    iterator.setText( paragraph );
172
173
    while( began < length && iterator.isBoundary( began + 1 ) ) {
174
      began++;
175
    }
176
177
    while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
178
      ended--;
179
    }
180
181
    return new int[]{began, ended};
182
  }
183
184
  /**
185
   * Decorates a {@link TreeItem} using the syntax specific to the type of
186
   * document being edited.
187
   *
188
   * @param leaf The path to the leaf (the definition key) to be decorated.
189
   */
190
  private String decorate( final DefinitionTreeItem<String> leaf ) {
191
    return decorate( leaf.toPath() );
192
  }
193
194
  /**
195
   * Decorates a variable using the syntax specific to the type of document
196
   * being edited.
197
   *
198
   * @param variable The variable to decorate in dot-notation without any
199
   *                 start or end sigils present.
200
   */
201
  private String decorate( final String variable ) {
202
    return getVariableDecorator().apply( variable );
203
  }
204
205
  /**
206
   * Updates the text at the given position within the current paragraph.
207
   *
208
   * @param posBegan The starting index in the paragraph text to replace.
209
   * @param posEnded The ending index in the paragraph text to replace.
210
   * @param text     Overwrite the paragraph substring with this text.
211
   */
212
  private void replaceText(
213
      final int posBegan, final int posEnded, final String text ) {
214
    final int p = getCurrentParagraph();
215
216
    getEditor().replaceText( p, posBegan, p, posEnded, text );
217
  }
218
219
  /**
220
   * Returns the caret's current paragraph position.
221
   *
222
   * @return A number greater than or equal to 0.
223
   */
224
  private int getCurrentParagraph() {
225
    return getEditor().getCurrentParagraph();
226
  }
227
228
  /**
229
   * Returns the text for the paragraph that contains the caret.
230
   *
231
   * @return A non-null string, possibly empty.
232
   */
233
  private String getCaretParagraph() {
234
    return getEditor().getText( getCurrentParagraph() );
235
  }
236
237
  /**
238
   * Returns the caret position within the current paragraph.
239
   *
240
   * @return A value from 0 to the length of the current paragraph.
241
   */
242
  private int getCurrentCaretColumn() {
243
    return getEditor().getCaretColumn();
244
  }
245
246
  /**
247
   * Looks for the given word, matching first by exact, next by a starts-with
248
   * condition with diacritics replaced, then by containment.
249
   *
250
   * @param word The word to match by: exact, at the beginning, or containment.
251
   * @return The matching {@link DefinitionTreeItem} for the given word, or
252
   * {@code null} if none found.
253
   */
254
  @SuppressWarnings("ConstantConditions")
255
  private DefinitionTreeItem<String> findLeaf( final String word ) {
256
    assert word != null;
257
258
    final var pane = getDefinitionPane();
259
    DefinitionTreeItem<String> leaf = null;
260
261
    leaf = leaf == null ? pane.findLeafExact( word ) : leaf;
262
    leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf;
263
    leaf = leaf == null ? pane.findLeafContains( word ) : leaf;
264
    leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf;
265
266
    return leaf;
267
  }
268
269
  /**
270
   * Answers whether there are any definitions in the tree.
271
   *
272
   * @return {@code true} when there are no definitions; {@code false} when
273
   * there's at least one definition.
274
   */
275
  private boolean isEmptyDefinitionPane() {
276
    return getDefinitionPane().isEmpty();
277
  }
278
279
  /**
280
   * Collapses the tree then expands and selects the given node.
281
   *
282
   * @param node The node to expand.
283
   */
284
  private void expand( final TreeItem<String> node ) {
285
    final DefinitionPane pane = getDefinitionPane();
286
    pane.collapse();
287
    pane.expand( node );
288
    pane.select( node );
289
  }
290
291
  /**
292
   * @return A variable decorator that corresponds to the given file type.
293
   */
294
  private SigilOperator getVariableDecorator() {
295
    return DefinitionDecoratorFactory.newInstance( getFilename() );
296
  }
297
298
  private Path getFilename() {
299
    return getFileEditorTab().getPath();
300
  }
301
302
  private EditorPane getEditorPane() {
303
    return getFileEditorTab().getEditorPane();
304
  }
305
306
  private StyledTextArea<?, ?> getEditor() {
307
    return getEditorPane().getEditor();
308
  }
309
310
  public FileEditorTab getFileEditorTab() {
311
    return mTab;
312
  }
313
314
  private DefinitionPane getDefinitionPane() {
315
    return mDefinitionPane;
316
  }
317
}
3181
D src/main/java/com/scrivenvar/editors/EditorPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.preferences.UserPreferences;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.ObjectProperty;
33
import javafx.beans.property.SimpleObjectProperty;
34
import javafx.beans.value.ChangeListener;
35
import javafx.event.Event;
36
import javafx.scene.control.ScrollPane;
37
import javafx.scene.layout.Pane;
38
import org.fxmisc.flowless.VirtualizedScrollPane;
39
import org.fxmisc.richtext.StyleClassedTextArea;
40
import org.fxmisc.undo.UndoManager;
41
import org.fxmisc.wellbehaved.event.EventPattern;
42
import org.fxmisc.wellbehaved.event.Nodes;
43
44
import java.nio.file.Path;
45
import java.util.function.Consumer;
46
47
import static com.scrivenvar.StatusBarNotifier.clearAlert;
48
import static java.lang.String.format;
49
import static javafx.application.Platform.runLater;
50
import static org.fxmisc.wellbehaved.event.InputMap.consume;
51
52
/**
53
 * Represents common editing features for various types of text editors.
54
 */
55
public class EditorPane extends Pane {
56
57
  /**
58
   * Used when changing the text area font size.
59
   */
60
  private static final String FMT_CSS_FONT_SIZE = "-fx-font-size: %dpt;";
61
62
  private final StyleClassedTextArea mEditor =
63
      new StyleClassedTextArea( false );
64
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
65
      new VirtualizedScrollPane<>( mEditor );
66
  private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>();
67
68
  public EditorPane() {
69
    getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
70
    fontsSizeProperty().addListener(
71
        ( l, o, n ) -> setFontSize( n.intValue() )
72
    );
73
74
    // Clear out any previous alerts after the user has typed. If the problem
75
    // persists, re-rendering the document will re-raise the error. If there
76
    // was no previous error, clearing the alert is essentially a no-op.
77
    mEditor.textProperty().addListener(
78
        ( l, o, n ) -> clearAlert()
79
    );
80
  }
81
82
  @Override
83
  public void requestFocus() {
84
    requestFocus( 3 );
85
  }
86
87
  /**
88
   * There's a race-condition between displaying the {@link EditorPane}
89
   * and giving the {@link #mEditor} focus. Try to focus up to {@code max}
90
   * times before giving up.
91
   *
92
   * @param max The number of attempts to try to request focus.
93
   */
94
  private void requestFocus( final int max ) {
95
    if( max > 0 ) {
96
      runLater(
97
          () -> {
98
            final var editor = getEditor();
99
100
            if( !editor.isFocused() ) {
101
              editor.requestFocus();
102
              requestFocus( max - 1 );
103
            }
104
          }
105
      );
106
    }
107
  }
108
109
  public void undo() {
110
    getUndoManager().undo();
111
  }
112
113
  public void redo() {
114
    getUndoManager().redo();
115
  }
116
117
  /**
118
   * Cuts the actively selected text; if no text is selected, this will cut
119
   * the entire paragraph.
120
   */
121
  public void cut() {
122
    final var editor = getEditor();
123
    final var selected = editor.getSelectedText();
124
125
    if( selected == null || selected.isEmpty() ) {
126
      editor.selectParagraph();
127
    }
128
129
    editor.cut();
130
  }
131
132
  public void copy() {
133
    getEditor().copy();
134
  }
135
136
  public void paste() {
137
    getEditor().paste();
138
  }
139
140
  public void selectAll() {
141
    getEditor().selectAll();
142
  }
143
144
  public UndoManager<?> getUndoManager() {
145
    return getEditor().getUndoManager();
146
  }
147
148
  public String getText() {
149
    return getEditor().getText();
150
  }
151
152
  public void setText( final String text ) {
153
    final var editor = getEditor();
154
    editor.deselect();
155
    editor.replaceText( text );
156
    getUndoManager().mark();
157
  }
158
159
  /**
160
   * Call to hook into changes to the text area.
161
   *
162
   * @param listener Receives editor text change events.
163
   */
164
  public void addTextChangeListener(
165
      final ChangeListener<? super String> listener ) {
166
    getEditor().textProperty().addListener( listener );
167
  }
168
169
  /**
170
   * Notifies observers when the caret changes paragraph.
171
   *
172
   * @param listener Receives change event.
173
   */
174
  public void addCaretParagraphListener(
175
      final ChangeListener<? super Integer> listener ) {
176
    getEditor().currentParagraphProperty().addListener( listener );
177
  }
178
179
  /**
180
   * Notifies observers when the caret changes position.
181
   *
182
   * @param listener Receives change event.
183
   */
184
  public void addCaretPositionListener(
185
      final ChangeListener<? super Integer> listener ) {
186
    getEditor().caretPositionProperty().addListener( listener );
187
  }
188
189
  /**
190
   * This method adds listeners to editor events.
191
   *
192
   * @param <T>      The event type.
193
   * @param <U>      The consumer type for the given event type.
194
   * @param event    The event of interest.
195
   * @param consumer The method to call when the event happens.
196
   */
197
  public <T extends Event, U extends T> void addKeyboardListener(
198
      final EventPattern<? super T, ? extends U> event,
199
      final Consumer<? super U> consumer ) {
200
    Nodes.addInputMap( getEditor(), consume( event, consumer ) );
201
  }
202
203
  /**
204
   * Repositions the cursor and scroll bar to the top of the file.
205
   */
206
  public void scrollToTop() {
207
    getEditor().moveTo( 0 );
208
    getScrollPane().scrollYToPixel( 0 );
209
  }
210
211
  public StyleClassedTextArea getEditor() {
212
    return mEditor;
213
  }
214
215
  /**
216
   * Returns the scroll pane that contains the text area.
217
   *
218
   * @return The scroll pane that contains the content to edit.
219
   */
220
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
221
    return mScrollPane;
222
  }
223
224
  public Path getPath() {
225
    return mPath.get();
226
  }
227
228
  public void setPath( final Path path ) {
229
    mPath.set( path );
230
  }
231
232
  /**
233
   * Sets the font size in points.
234
   *
235
   * @param size The new font size to use for the text editor.
236
   */
237
  private void setFontSize( final int size ) {
238
    mEditor.setStyle( format( FMT_CSS_FONT_SIZE, size ) );
239
  }
240
241
  /**
242
   * Returns the text editor font size property for handling font size change
243
   * events.
244
   */
245
  private IntegerProperty fontsSizeProperty() {
246
    return UserPreferences.getInstance().fontsSizeEditorProperty();
247
  }
248
}
2491
D src/main/java/com/scrivenvar/editors/markdown/HyperlinkModel.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
32
/**
33
 * Represents the model for a hyperlink: text, url, and title.
34
 */
35
public class HyperlinkModel {
36
37
  private String text;
38
  private String url;
39
  private String title;
40
41
  /**
42
   * Constructs a new hyperlink model in Markdown format by default with no
43
   * title (i.e., tooltip).
44
   *
45
   * @param text The hyperlink text displayed (e.g., displayed to the user).
46
   * @param url  The destination URL (e.g., when clicked).
47
   */
48
  public HyperlinkModel( final String text, final String url ) {
49
    this( text, url, null );
50
  }
51
52
  /**
53
   * Constructs a new hyperlink model for the given AST link.
54
   *
55
   * @param link A markdown link.
56
   */
57
  public HyperlinkModel( final Link link ) {
58
    this(
59
        link.getText().toString(),
60
        link.getUrl().toString(),
61
        link.getTitle().toString()
62
    );
63
  }
64
65
  /**
66
   * Constructs a new hyperlink model in Markdown format by default.
67
   *
68
   * @param text  The hyperlink text displayed (e.g., displayed to the user).
69
   * @param url   The destination URL (e.g., when clicked).
70
   * @param title The hyperlink title (e.g., shown as a tooltip).
71
   */
72
  public HyperlinkModel( final String text, final String url,
73
                         final String title ) {
74
    setText( text );
75
    setUrl( url );
76
    setTitle( title );
77
  }
78
79
  /**
80
   * Returns the string in Markdown format by default.
81
   *
82
   * @return A markdown version of the hyperlink.
83
   */
84
  @Override
85
  public String toString() {
86
    String format = "%s%s%s";
87
88
    if( hasText() ) {
89
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
90
    }
91
92
    // Becomes ""+URL+"" if no text is set.
93
    // Becomes [TITLE]+(URL)+"" if no title is set.
94
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
95
    return String.format( format, getText(), getUrl(), getTitle() );
96
  }
97
98
  public final void setText( final String text ) {
99
    this.text = nullSafe( text );
100
  }
101
102
  public final void setUrl( final String url ) {
103
    this.url = nullSafe( url );
104
  }
105
106
  public final void setTitle( final String title ) {
107
    this.title = nullSafe( title );
108
  }
109
110
  /**
111
   * Answers whether text has been set for the hyperlink.
112
   *
113
   * @return true This is a text link.
114
   */
115
  public boolean hasText() {
116
    return !getText().isEmpty();
117
  }
118
119
  /**
120
   * Answers whether a title (tooltip) has been set for the hyperlink.
121
   *
122
   * @return true There is a title.
123
   */
124
  public boolean hasTitle() {
125
    return !getTitle().isEmpty();
126
  }
127
128
  public String getText() {
129
    return this.text;
130
  }
131
132
  public String getUrl() {
133
    return this.url;
134
  }
135
136
  public String getTitle() {
137
    return this.title;
138
  }
139
140
  private String nullSafe( final String s ) {
141
    return s == null ? "" : s;
142
  }
143
}
1441
D src/main/java/com/scrivenvar/editors/markdown/LinkVisitor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
import com.vladsch.flexmark.util.ast.Node;
32
import com.vladsch.flexmark.util.ast.NodeVisitor;
33
import com.vladsch.flexmark.util.ast.VisitHandler;
34
35
/**
36
 * Responsible for extracting a hyperlink from the document so that the user
37
 * can edit the link within a dialog.
38
 */
39
public class LinkVisitor {
40
41
  private NodeVisitor visitor;
42
  private Link link;
43
  private final int offset;
44
45
  /**
46
   * Creates a hyperlink given an offset into a paragraph and the markdown AST
47
   * link node.
48
   *
49
   * @param index Index into the paragraph that indicates the hyperlink to
50
   *              change.
51
   */
52
  public LinkVisitor( final int index ) {
53
    this.offset = index;
54
  }
55
56
  public Link process( final Node root ) {
57
    getVisitor().visit( root );
58
    return getLink();
59
  }
60
61
  /**
62
   * @param link Not null.
63
   */
64
  private void visit( final Link link ) {
65
    final int began = link.getStartOffset();
66
    final int ended = link.getEndOffset();
67
    final int index = getOffset();
68
69
    if( index >= began && index <= ended ) {
70
      setLink( link );
71
    }
72
  }
73
74
  private synchronized NodeVisitor getVisitor() {
75
    if( this.visitor == null ) {
76
      this.visitor = createVisitor();
77
    }
78
79
    return this.visitor;
80
  }
81
82
  protected NodeVisitor createVisitor() {
83
    return new NodeVisitor(
84
        new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
85
  }
86
87
  private Link getLink() {
88
    return this.link;
89
  }
90
91
  private void setLink( final Link link ) {
92
    this.link = link;
93
  }
94
95
  public int getOffset() {
96
    return this.offset;
97
  }
98
}
991
D src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.scrivenvar.dialogs.ImageDialog;
31
import com.scrivenvar.dialogs.LinkDialog;
32
import com.scrivenvar.editors.EditorPane;
33
import com.scrivenvar.processors.markdown.BlockExtension;
34
import com.scrivenvar.processors.markdown.MarkdownProcessor;
35
import com.vladsch.flexmark.ast.Link;
36
import com.vladsch.flexmark.html.renderer.AttributablePart;
37
import com.vladsch.flexmark.util.ast.Node;
38
import com.vladsch.flexmark.util.html.MutableAttributes;
39
import javafx.scene.control.Dialog;
40
import javafx.scene.control.IndexRange;
41
import javafx.scene.input.KeyCode;
42
import javafx.scene.input.KeyEvent;
43
import javafx.stage.Window;
44
import org.fxmisc.richtext.StyleClassedTextArea;
45
46
import java.nio.file.Path;
47
import java.util.ArrayList;
48
import java.util.List;
49
import java.util.regex.Matcher;
50
import java.util.regex.Pattern;
51
52
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
53
import static com.scrivenvar.util.Utils.ltrim;
54
import static com.scrivenvar.util.Utils.rtrim;
55
import static javafx.scene.input.KeyCode.ENTER;
56
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
57
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
58
59
/**
60
 * Provides the ability to edit a text document.
61
 */
62
public class MarkdownEditorPane extends EditorPane {
63
  private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
64
      "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
65
66
  /**
67
   * Any of these followed by a space and a letter produce a line
68
   * by themselves. The ">" need not be followed by a space.
69
   */
70
  private static final Pattern PATTERN_NEW_LINE = Pattern.compile(
71
      "^>|(((#+)|([*+\\-])|([1-9]\\.))\\s+).+" );
72
73
  public MarkdownEditorPane() {
74
    initEditor();
75
  }
76
77
  private void initEditor() {
78
    final StyleClassedTextArea textArea = getEditor();
79
80
    textArea.setWrapText( true );
81
    textArea.getStyleClass().add( "markdown-editor" );
82
    textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
83
84
    addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
85
    addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut );
86
  }
87
88
  public void insertLink() {
89
    insertObject( createLinkDialog() );
90
  }
91
92
  public void insertImage() {
93
    insertObject( createImageDialog() );
94
  }
95
96
  /**
97
   * Returns the editor's paragraph number that will be close to its HTML
98
   * paragraph ID. Ultimately this solution is flawed because there isn't
99
   * a straightforward correlation between the document being edited and
100
   * what is rendered. XML documents transformed through stylesheets have
101
   * no readily determined correlation. Images, tables, and other
102
   * objects affect the relative location of the current paragraph being
103
   * edited with respect to the preview pane.
104
   * <p>
105
   * See
106
   * {@link BlockExtension.IdAttributeProvider#setAttributes(Node, AttributablePart, MutableAttributes)}}
107
   * for details.
108
   * </p>
109
   * <p>
110
   * Injecting a token into the document, as per a previous version of the
111
   * application, can instruct the preview pane where to shift the viewport.
112
   * </p>
113
   *
114
   * @param paraIndex The paragraph index from the editor pane to scroll to
115
   *                  in the preview pane, which  will be approximated if an
116
   *                  equivalent cannot be found.
117
   * @return A unique identifier that correlates to an equivalent paragraph
118
   * number once the Markdown is rendered into HTML.
119
   */
120
  public int approximateParagraphId( final int paraIndex ) {
121
    final StyleClassedTextArea editor = getEditor();
122
    final List<String> lines = new ArrayList<>( 4096 );
123
124
    int i = 0;
125
    String prevText = "";
126
    boolean withinFencedBlock = false;
127
    boolean withinCodeBlock = false;
128
129
    for( final var p : editor.getParagraphs() ) {
130
      if( i > paraIndex ) {
131
        break;
132
      }
133
134
      final String text = p.getText().replace( '>', ' ' );
135
      if( text.startsWith( "```" ) ) {
136
        if( withinFencedBlock = !withinFencedBlock ) {
137
          lines.add( text );
138
        }
139
      }
140
141
      if( !withinFencedBlock ) {
142
        final boolean foundCodeBlock = text.startsWith( "    " );
143
144
        if( foundCodeBlock && !withinCodeBlock ) {
145
          lines.add( text );
146
          withinCodeBlock = true;
147
        }
148
        else if( !foundCodeBlock ) {
149
          withinCodeBlock = false;
150
        }
151
      }
152
153
      if( !withinFencedBlock && !withinCodeBlock &&
154
          ((!text.isBlank() && prevText.isBlank()) ||
155
              PATTERN_NEW_LINE.matcher( text ).matches()) ) {
156
        lines.add( text );
157
      }
158
159
      prevText = text;
160
      i++;
161
    }
162
163
    // Scrolling index is 1-based.
164
    return Math.max( lines.size() - 1, 0 );
165
  }
166
167
  /**
168
   * Gets the index of the paragraph where the caret is positioned.
169
   *
170
   * @return The paragraph number for the caret.
171
   */
172
  public int getCurrentParagraphIndex() {
173
    return getEditor().getCurrentParagraph();
174
  }
175
176
  /**
177
   * @param leading  Characters to insert at the beginning of the current
178
   *                 selection (or paragraph).
179
   * @param trailing Characters to insert at the end of the current selection
180
   *                 (or paragraph).
181
   */
182
  public void surroundSelection( final String leading, final String trailing ) {
183
    surroundSelection( leading, trailing, null );
184
  }
185
186
  /**
187
   * @param leading  Characters to insert at the beginning of the current
188
   *                 selection (or paragraph).
189
   * @param trailing Characters to insert at the end of the current selection
190
   *                 (or paragraph).
191
   * @param hint     Instructional text inserted within the leading and
192
   *                 trailing characters, provided no text is selected.
193
   */
194
  public void surroundSelection(
195
      String leading, String trailing, final String hint ) {
196
    final StyleClassedTextArea textArea = getEditor();
197
198
    // Note: not using textArea.insertText() to insert leading and trailing
199
    // because this would add two changes to undo history
200
    final IndexRange selection = textArea.getSelection();
201
    int start = selection.getStart();
202
    int end = selection.getEnd();
203
204
    final String selectedText = textArea.getSelectedText();
205
206
    String trimmedText = selectedText.trim();
207
    if( trimmedText.length() < selectedText.length() ) {
208
      start += selectedText.indexOf( trimmedText );
209
      end = start + trimmedText.length();
210
    }
211
212
    // remove leading whitespaces from leading text if selection starts at zero
213
    if( start == 0 ) {
214
      leading = ltrim( leading );
215
    }
216
217
    // remove trailing whitespaces from trailing text if selection ends at
218
    // text end
219
    if( end == textArea.getLength() ) {
220
      trailing = rtrim( trailing );
221
    }
222
223
    // remove leading line separators from leading text
224
    // if there are line separators before the selected text
225
    if( leading.startsWith( "\n" ) ) {
226
      for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
227
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
228
          break;
229
        }
230
231
        leading = leading.substring( 1 );
232
      }
233
    }
234
235
    // remove trailing line separators from trailing or leading text
236
    // if there are line separators after the selected text
237
    final boolean trailingIsEmpty = trailing.isEmpty();
238
    String str = trailingIsEmpty ? leading : trailing;
239
240
    if( str.endsWith( "\n" ) ) {
241
      final int length = textArea.getLength();
242
243
      for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
244
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
245
          break;
246
        }
247
248
        str = str.substring( 0, str.length() - 1 );
249
      }
250
251
      if( trailingIsEmpty ) {
252
        leading = str;
253
      }
254
      else {
255
        trailing = str;
256
      }
257
    }
258
259
    int selStart = start + leading.length();
260
    int selEnd = end + leading.length();
261
262
    // insert hint text if selection is empty
263
    if( hint != null && trimmedText.isEmpty() ) {
264
      trimmedText = hint;
265
      selEnd = selStart + hint.length();
266
    }
267
268
    // prevent undo merging with previous text entered by user
269
    getUndoManager().preventMerge();
270
271
    // replace text and update selection
272
    textArea.replaceText( start, end, leading + trimmedText + trailing );
273
    textArea.selectRange( selStart, selEnd );
274
  }
275
276
  private void enterPressed( final KeyEvent e ) {
277
    final StyleClassedTextArea textArea = getEditor();
278
    final String currentLine =
279
        textArea.getText( textArea.getCurrentParagraph() );
280
    final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
281
282
    String newText = "\n";
283
284
    if( matcher.matches() ) {
285
      if( !matcher.group( 2 ).isEmpty() ) {
286
        // indent new line with same whitespace characters and list markers
287
        // as current line
288
        newText = newText.concat( matcher.group( 1 ) );
289
      }
290
      else {
291
        // current line contains only whitespace characters and list markers
292
        // --> empty current line
293
        final int caretPosition = textArea.getCaretPosition();
294
        textArea.selectRange( caretPosition - currentLine.length(),
295
                              caretPosition );
296
      }
297
    }
298
299
    textArea.replaceSelection( newText );
300
301
    // Ensure that the window scrolls when Enter is pressed at the bottom of
302
    // the pane.
303
    textArea.requestFollowCaret();
304
  }
305
306
  private void cut( final KeyEvent event ) {
307
    super.cut();
308
  }
309
310
  /**
311
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
312
   * the markdown AST.
313
   *
314
   * @return An instance containing the link URL and display text.
315
   */
316
  private HyperlinkModel getHyperlink() {
317
    final StyleClassedTextArea textArea = getEditor();
318
    final String selectedText = textArea.getSelectedText();
319
320
    // Get the current paragraph, convert to Markdown nodes.
321
    final MarkdownProcessor mp = new MarkdownProcessor( null );
322
    final int p = textArea.getCurrentParagraph();
323
    final String paragraph = textArea.getText( p );
324
    final Node node = mp.toNode( paragraph );
325
    final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
326
    final Link link = visitor.process( node );
327
328
    if( link != null ) {
329
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
330
    }
331
332
    return createHyperlinkModel(
333
        link, selectedText, "https://localhost"
334
    );
335
  }
336
337
  @SuppressWarnings("SameParameterValue")
338
  private HyperlinkModel createHyperlinkModel(
339
      final Link link, final String selection, final String url ) {
340
341
    return link == null
342
        ? new HyperlinkModel( selection, url )
343
        : new HyperlinkModel( link );
344
  }
345
346
  private Path getParentPath() {
347
    final Path path = getPath();
348
    return (path != null) ? path.getParent() : null;
349
  }
350
351
  private Dialog<String> createLinkDialog() {
352
    return new LinkDialog( getWindow(), getHyperlink() );
353
  }
354
355
  private Dialog<String> createImageDialog() {
356
    return new ImageDialog( getWindow(), getParentPath() );
357
  }
358
359
  private void insertObject( final Dialog<String> dialog ) {
360
    dialog.showAndWait().ifPresent(
361
        result -> getEditor().replaceSelection( result )
362
    );
363
  }
364
365
  private Window getWindow() {
366
    return getScrollPane().getScene().getWindow();
367
  }
368
}
3691
D src/main/java/com/scrivenvar/predicates/PredicateFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates;
29
30
import java.io.File;
31
import java.util.Collection;
32
import java.util.function.Predicate;
33
34
import static java.lang.String.join;
35
import static java.nio.file.FileSystems.getDefault;
36
37
/**
38
 * Provides a number of simple {@link Predicate} instances for various types
39
 * of string comparisons, including basic strings and file name strings.
40
 */
41
public class PredicateFactory {
42
  /**
43
   * Creates an instance of {@link Predicate} that matches a globbed file
44
   * name pattern.
45
   *
46
   * @param pattern The file name pattern to match.
47
   * @return A {@link Predicate} that can answer whether a given file name
48
   * matches the given glob pattern.
49
   */
50
  public static Predicate<File> createFileTypePredicate(
51
      final String pattern ) {
52
    final var matcher = getDefault().getPathMatcher(
53
        "glob:**{" + pattern + "}"
54
    );
55
56
    return file -> matcher.matches( file.toPath() );
57
  }
58
59
  /**
60
   * Creates an instance of {@link Predicate} that matches any file name from
61
   * a {@link Collection} of file name patterns. The given patterns are joined
62
   * with commas into a single comma-separated list.
63
   *
64
   * @param patterns The file name patterns to be matched.
65
   * @return A {@link Predicate} that can answer whether a given file name
66
   * matches the given glob patterns.
67
   */
68
  public static Predicate<File> createFileTypePredicate(
69
      final Collection<String> patterns ) {
70
    return createFileTypePredicate( join( ",", patterns ) );
71
  }
72
73
  /**
74
   * Creates an instance of {@link Predicate} that compares whether the given
75
   * {@code reference} string is contained by the comparator. Comparison is
76
   * case-insensitive. The test will also pass if the comparate is empty.
77
   *
78
   * @param comparator The string to check as being contained.
79
   * @return A {@link Predicate} that can answer whether the given string
80
   * is contained within the comparator, or the comparate is empty.
81
   */
82
  public static Predicate<String> createStringContainsPredicate(
83
      final String comparator ) {
84
    return comparate -> comparate.isEmpty() ||
85
        comparate.toLowerCase().contains( comparator.toLowerCase() );
86
  }
871
88
  /**
89
   * Creates an instance of {@link Predicate} that compares whether the given
90
   * {@code reference} string is starts with the comparator. Comparison is
91
   * case-insensitive.
92
   *
93
   * @param comparator The string to check as being contained.
94
   * @return A {@link Predicate} that can answer whether the given string
95
   * is contained within the comparator.
96
   */
97
  public static Predicate<String> createStringStartsPredicate(
98
      final String comparator ) {
99
    return comparate ->
100
        comparate.toLowerCase().startsWith( comparator.toLowerCase() );
101
  }
102
}
D src/main/java/com/scrivenvar/preferences/FilePreferences.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import java.io.File;
31
import java.io.FileInputStream;
32
import java.io.FileOutputStream;
33
import java.util.*;
34
import java.util.prefs.AbstractPreferences;
35
import java.util.prefs.BackingStoreException;
36
37
import static com.scrivenvar.StatusBarNotifier.alert;
38
39
/**
40
 * Preferences implementation that stores to a user-defined file. Local file
41
 * storage is preferred over a certain operating system's monolithic trash heap
42
 * called a registry. When the OS is locked down, the default Preferences
43
 * implementation will try to write to the registry and fail due to permissions
44
 * problems. This class sidesteps the issue entirely by writing to the user's
45
 * home directory, where permissions should be a bit more lax.
46
 */
47
public class FilePreferences extends AbstractPreferences {
48
49
  private final Map<String, String> mRoot = new TreeMap<>();
50
  private final Map<String, FilePreferences> mChildren = new TreeMap<>();
51
  private boolean mRemoved;
52
53
  private final Object mMutex = new Object();
54
55
  public FilePreferences(
56
      final AbstractPreferences parent, final String name ) {
57
    super( parent, name );
58
59
    try {
60
      sync();
61
    } catch( final BackingStoreException ex ) {
62
      alert( ex );
63
    }
64
  }
65
66
  @Override
67
  protected void putSpi( final String key, final String value ) {
68
    synchronized( mMutex ) {
69
      mRoot.put( key, value );
70
    }
71
72
    try {
73
      flush();
74
    } catch( final BackingStoreException ex ) {
75
      alert( ex );
76
    }
77
  }
78
79
  @Override
80
  protected String getSpi( final String key ) {
81
    synchronized( mMutex ) {
82
      return mRoot.get( key );
83
    }
84
  }
85
86
  @Override
87
  protected void removeSpi( final String key ) {
88
    synchronized( mMutex ) {
89
      mRoot.remove( key );
90
    }
91
92
    try {
93
      flush();
94
    } catch( final BackingStoreException ex ) {
95
      alert( ex );
96
    }
97
  }
98
99
  @Override
100
  protected void removeNodeSpi() throws BackingStoreException {
101
    mRemoved = true;
102
    flush();
103
  }
104
105
  @Override
106
  protected String[] keysSpi() {
107
    synchronized( mMutex ) {
108
      return mRoot.keySet().toArray( new String[ 0 ] );
109
    }
110
  }
111
112
  @Override
113
  protected String[] childrenNamesSpi() {
114
    return mChildren.keySet().toArray( new String[ 0 ] );
115
  }
116
117
  @Override
118
  protected FilePreferences childSpi( final String name ) {
119
    FilePreferences child = mChildren.get( name );
120
121
    if( child == null || child.isRemoved() ) {
122
      child = new FilePreferences( this, name );
123
      mChildren.put( name, child );
124
    }
125
126
    return child;
127
  }
128
129
  @Override
130
  protected void syncSpi() {
131
    if( isRemoved() ) {
132
      return;
133
    }
134
135
    final File file = FilePreferencesFactory.getPreferencesFile();
136
137
    if( !file.exists() ) {
138
      return;
139
    }
140
141
    synchronized( mMutex ) {
142
      final Properties p = new Properties();
143
144
      try( final var inputStream = new FileInputStream( file ) ) {
145
        p.load( inputStream );
146
147
        final String path = getPath();
148
        final Enumeration<?> propertyNames = p.propertyNames();
149
150
        while( propertyNames.hasMoreElements() ) {
151
          final String propKey = (String) propertyNames.nextElement();
152
153
          if( propKey.startsWith( path ) ) {
154
            final String subKey = propKey.substring( path.length() );
155
156
            // Only load immediate descendants
157
            if( subKey.indexOf( '.' ) == -1 ) {
158
              mRoot.put( subKey, p.getProperty( propKey ) );
159
            }
160
          }
161
        }
162
      } catch( final Exception ex ) {
163
        alert( ex );
164
      }
165
    }
166
  }
167
168
  private String getPath() {
169
    final FilePreferences parent = (FilePreferences) parent();
170
171
    return parent == null ? "" : parent.getPath() + name() + '.';
172
  }
173
174
  @Override
175
  protected void flushSpi() {
176
    final File file = FilePreferencesFactory.getPreferencesFile();
177
178
    synchronized( mMutex ) {
179
      final Properties p = new Properties();
180
181
      try {
182
        final String path = getPath();
183
184
        if( file.exists() ) {
185
          try( final var fis = new FileInputStream( file ) ) {
186
            p.load( fis );
187
          }
188
189
          final List<String> toRemove = new ArrayList<>();
190
191
          // Make a list of all direct children of this node to be removed
192
          final Enumeration<?> propertyNames = p.propertyNames();
193
194
          while( propertyNames.hasMoreElements() ) {
195
            final String propKey = (String) propertyNames.nextElement();
196
            if( propKey.startsWith( path ) ) {
197
              final String subKey = propKey.substring( path.length() );
198
199
              // Only do immediate descendants
200
              if( subKey.indexOf( '.' ) == -1 ) {
201
                toRemove.add( propKey );
202
              }
203
            }
204
          }
205
206
          // Remove them now that the enumeration is done with
207
          for( final String propKey : toRemove ) {
208
            p.remove( propKey );
209
          }
210
        }
211
212
        // If this node hasn't been removed, add back in any values
213
        if( !mRemoved ) {
214
          for( final String s : mRoot.keySet() ) {
215
            p.setProperty( path + s, mRoot.get( s ) );
216
          }
217
        }
218
219
        try( final var fos = new FileOutputStream( file ) ) {
220
          p.store( fos, "FilePreferences" );
221
        }
222
      } catch( final Exception ex ) {
223
        alert( ex );
224
      }
225
    }
226
  }
227
}
2281
D src/main/java/com/scrivenvar/preferences/FilePreferencesFactory.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import java.io.File;
31
import java.nio.file.FileSystems;
32
import java.util.prefs.Preferences;
33
import java.util.prefs.PreferencesFactory;
34
35
import static com.scrivenvar.Constants.APP_TITLE;
36
37
/**
38
 * PreferencesFactory implementation that stores the preferences in a
39
 * user-defined file. Usage:
40
 * <pre>
41
 * System.setProperty( "java.util.prefs.PreferencesFactory",
42
 * FilePreferencesFactory.class.getName() );
43
 * </pre>
44
 * <p>
45
 * The file defaults to <code>$user.home/.scrivenvar</code>, but can be changed
46
 * using <code>-Dapplication.name=preferences</code> when running the
47
 * application, or by calling <code>System.setProperty</code> with the
48
 * "application.name" property.
49
 * </p>
50
 */
51
public class FilePreferencesFactory implements PreferencesFactory {
52
53
  private static File preferencesFile;
54
  private Preferences rootPreferences;
55
56
  @Override
57
  public Preferences systemRoot() {
58
    return userRoot();
59
  }
60
61
  @Override
62
  public synchronized Preferences userRoot() {
63
    if( rootPreferences == null ) {
64
      rootPreferences = new FilePreferences( null, "" );
65
    }
66
67
    return rootPreferences;
68
  }
69
70
  public synchronized static File getPreferencesFile() {
71
    if( preferencesFile == null ) {
72
      String prefsFile = getPreferencesFilename();
73
74
      preferencesFile = new File( prefsFile ).getAbsoluteFile();
75
    }
76
77
    return preferencesFile;
78
  }
79
80
  public static String getPreferencesFilename() {
81
    final String filename = System.getProperty( "application.name", APP_TITLE );
82
    return System.getProperty( "user.home" ) + getSeparator() + "." + filename;
83
  }
84
85
  public static String getSeparator() {
86
    return FileSystems.getDefault().getSeparator();
87
  }
88
}
891
D src/main/java/com/scrivenvar/preferences/UserPreferences.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import com.dlsc.formsfx.model.structure.StringField;
31
import com.dlsc.preferencesfx.PreferencesFx;
32
import com.dlsc.preferencesfx.PreferencesFxEvent;
33
import com.dlsc.preferencesfx.model.Category;
34
import com.dlsc.preferencesfx.model.Group;
35
import com.dlsc.preferencesfx.model.Setting;
36
import javafx.beans.property.*;
37
import javafx.event.EventHandler;
38
import javafx.scene.Node;
39
import javafx.scene.control.Label;
40
41
import java.io.File;
42
import java.nio.file.Path;
43
44
import static com.scrivenvar.Constants.*;
45
import static com.scrivenvar.Messages.get;
46
47
/**
48
 * Responsible for user preferences that can be changed from the GUI. The
49
 * settings are displayed and persisted using {@link PreferencesFx}.
50
 */
51
public class UserPreferences {
52
  /**
53
   * Implementation of the  initialization-on-demand holder design pattern,
54
   * an for a lazy-loaded singleton. In all versions of Java, the idiom enables
55
   * a safe, highly concurrent lazy initialization of static fields with good
56
   * performance. The implementation relies upon the initialization phase of
57
   * execution within the Java Virtual Machine (JVM) as specified by the Java
58
   * Language Specification. When the class {@link UserPreferencesContainer}
59
   * is loaded, its initialization completes trivially because there are no
60
   * static variables to initialize.
61
   * <p>
62
   * The static class definition {@link UserPreferencesContainer} within the
63
   * {@link UserPreferences} is not initialized until such time that
64
   * {@link UserPreferencesContainer} must be executed. The static
65
   * {@link UserPreferencesContainer} class executes when
66
   * {@link #getInstance} is called. The first call will trigger loading and
67
   * initialization of the {@link UserPreferencesContainer} thereby
68
   * instantiating the {@link #INSTANCE}.
69
   * </p>
70
   * <p>
71
   * This indirection is necessary because the {@link UserPreferences} class
72
   * references {@link PreferencesFx}, which must not be instantiated until the
73
   * UI is ready.
74
   * </p>
75
   */
76
  private static class UserPreferencesContainer {
77
    private static final UserPreferences INSTANCE = new UserPreferences();
78
  }
79
80
  public static UserPreferences getInstance() {
81
    return UserPreferencesContainer.INSTANCE;
82
  }
83
84
  private final PreferencesFx mPreferencesFx;
85
86
  private final ObjectProperty<File> mPropRDirectory;
87
  private final StringProperty mPropRScript;
88
  private final ObjectProperty<File> mPropImagesDirectory;
89
  private final StringProperty mPropImagesOrder;
90
  private final ObjectProperty<File> mPropDefinitionPath;
91
  private final StringProperty mRDelimiterBegan;
92
  private final StringProperty mRDelimiterEnded;
93
  private final StringProperty mDefDelimiterBegan;
94
  private final StringProperty mDefDelimiterEnded;
95
  private final IntegerProperty mPropFontsSizeEditor;
96
97
  private UserPreferences() {
98
    mPropRDirectory = simpleFile( USER_DIRECTORY );
99
    mPropRScript = new SimpleStringProperty( "" );
100
101
    mPropImagesDirectory = simpleFile( USER_DIRECTORY );
102
    mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT );
103
104
    mPropDefinitionPath = simpleFile(
105
        getSetting( "file.definition.default", DEFINITION_NAME )
106
    );
107
108
    mDefDelimiterBegan = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT );
109
    mDefDelimiterEnded = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT );
110
111
    mRDelimiterBegan = new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT );
112
    mRDelimiterEnded = new SimpleStringProperty( R_DELIM_ENDED_DEFAULT );
113
114
    mPropFontsSizeEditor = new SimpleIntegerProperty( (int) FONT_SIZE_EDITOR );
115
116
    // All properties must be initialized before creating the dialog.
117
    mPreferencesFx = createPreferencesFx();
118
  }
119
120
  /**
121
   * Display the user preferences settings dialog (non-modal).
122
   */
123
  public void show() {
124
    getPreferencesFx().show( false );
125
  }
126
127
  /**
128
   * Call to persist the settings. Strictly speaking, this could watch on
129
   * all values for external changes then save automatically.
130
   */
131
  public void save() {
132
    getPreferencesFx().saveSettings();
133
  }
134
135
  /**
136
   * Creates the preferences dialog.
137
   * <p>
138
   * TODO: Make this dynamic by iterating over all "Preferences.*" values
139
   * that follow a particular naming pattern.
140
   * </p>
141
   *
142
   * @return A new instance of preferences for users to edit.
143
   */
144
  @SuppressWarnings("unchecked")
145
  private PreferencesFx createPreferencesFx() {
146
    final Setting<StringField, StringProperty> scriptSetting =
147
        Setting.of( "Script", mPropRScript );
148
    final StringField field = scriptSetting.getElement();
149
    field.multiline( true );
150
151
    return PreferencesFx.of(
152
        UserPreferences.class,
153
        Category.of(
154
            get( "Preferences.r" ),
155
            Group.of(
156
                get( "Preferences.r.directory" ),
157
                Setting.of( label( "Preferences.r.directory.desc", false ) ),
158
                Setting.of( "Directory", mPropRDirectory, true )
159
            ),
160
            Group.of(
161
                get( "Preferences.r.script" ),
162
                Setting.of( label( "Preferences.r.script.desc" ) ),
163
                scriptSetting
164
            ),
165
            Group.of(
166
                get( "Preferences.r.delimiter.began" ),
167
                Setting.of( label( "Preferences.r.delimiter.began.desc" ) ),
168
                Setting.of( "Opening", mRDelimiterBegan )
169
            ),
170
            Group.of(
171
                get( "Preferences.r.delimiter.ended" ),
172
                Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ),
173
                Setting.of( "Closing", mRDelimiterEnded )
174
            )
175
        ),
176
        Category.of(
177
            get( "Preferences.images" ),
178
            Group.of(
179
                get( "Preferences.images.directory" ),
180
                Setting.of( label( "Preferences.images.directory.desc" ) ),
181
                Setting.of( "Directory", mPropImagesDirectory, true )
182
            ),
183
            Group.of(
184
                get( "Preferences.images.suffixes" ),
185
                Setting.of( label( "Preferences.images.suffixes.desc" ) ),
186
                Setting.of( "Extensions", mPropImagesOrder )
187
            )
188
        ),
189
        Category.of(
190
            get( "Preferences.definitions" ),
191
            Group.of(
192
                get( "Preferences.definitions.path" ),
193
                Setting.of( label( "Preferences.definitions.path.desc" ) ),
194
                Setting.of( "Path", mPropDefinitionPath, false )
195
            ),
196
            Group.of(
197
                get( "Preferences.definitions.delimiter.began" ),
198
                Setting.of( label(
199
                    "Preferences.definitions.delimiter.began.desc" ) ),
200
                Setting.of( "Opening", mDefDelimiterBegan )
201
            ),
202
            Group.of(
203
                get( "Preferences.definitions.delimiter.ended" ),
204
                Setting.of( label(
205
                    "Preferences.definitions.delimiter.ended.desc" ) ),
206
                Setting.of( "Closing", mDefDelimiterEnded )
207
            )
208
        ),
209
        Category.of(
210
            get( "Preferences.fonts" ),
211
            Group.of(
212
                get( "Preferences.fonts.size_editor" ),
213
                Setting.of( label( "Preferences.fonts.size_editor.desc" ) ),
214
                Setting.of( "Points", mPropFontsSizeEditor )
215
            )
216
        )
217
    ).instantPersistent( false );
218
  }
219
220
  /**
221
   * Wraps a {@link File} inside a {@link SimpleObjectProperty}.
222
   *
223
   * @param path The file name to use when constructing the {@link File}.
224
   * @return A new {@link SimpleObjectProperty} instance with a {@link File}
225
   * that references the given {@code path}.
226
   */
227
  private SimpleObjectProperty<File> simpleFile( final String path ) {
228
    return new SimpleObjectProperty<>( new File( path ) );
229
  }
230
231
  /**
232
   * Creates a label for the given key after interpolating its value.
233
   *
234
   * @param key The key to find in the resource bundle.
235
   * @return The value of the key as a label.
236
   */
237
  private Node label( final String key ) {
238
    return new Label( get( key, true ) );
239
  }
240
241
  /**
242
   * Creates a label for the given key.
243
   *
244
   * @param key         The key to find in the resource bundle.
245
   * @param interpolate {@code true} means to interpolate the value.
246
   * @return The value of the key, interpolated if {@code interpolate} is
247
   * {@code true}.
248
   */
249
  @SuppressWarnings("SameParameterValue")
250
  private Node label( final String key, final boolean interpolate ) {
251
    return new Label( get( key, interpolate ) );
252
  }
253
254
  /**
255
   * Delegates to the {@link PreferencesFx} event handler for monitoring
256
   * save events.
257
   *
258
   * @param eventHandler The handler to call when the preferences are saved.
259
   */
260
  public void addSaveEventHandler(
261
      final EventHandler<? super PreferencesFxEvent> eventHandler ) {
262
    final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
263
    getPreferencesFx().addEventHandler( eventType, eventHandler );
264
  }
265
266
  /**
267
   * Returns the value for a key from the settings properties file.
268
   *
269
   * @param key   Key within the settings properties file to find.
270
   * @param value Default value to return if the key is not found.
271
   * @return The value for the given key from the settings file, or the
272
   * given {@code value} if no key found.
273
   */
274
  @SuppressWarnings("SameParameterValue")
275
  private String getSetting( final String key, final String value ) {
276
    return SETTINGS.getSetting( key, value );
277
  }
278
279
  public ObjectProperty<File> definitionPathProperty() {
280
    return mPropDefinitionPath;
281
  }
282
283
  public Path getDefinitionPath() {
284
    return definitionPathProperty().getValue().toPath();
285
  }
286
287
  private StringProperty defDelimiterBegan() {
288
    return mDefDelimiterBegan;
289
  }
290
291
  public String getDefDelimiterBegan() {
292
    return defDelimiterBegan().get();
293
  }
294
295
  private StringProperty defDelimiterEnded() {
296
    return mDefDelimiterEnded;
297
  }
298
299
  public String getDefDelimiterEnded() {
300
    return defDelimiterEnded().get();
301
  }
302
303
  public ObjectProperty<File> rDirectoryProperty() {
304
    return mPropRDirectory;
305
  }
306
307
  public File getRDirectory() {
308
    return rDirectoryProperty().getValue();
309
  }
310
311
  public StringProperty rScriptProperty() {
312
    return mPropRScript;
313
  }
314
315
  public String getRScript() {
316
    return rScriptProperty().getValue();
317
  }
318
319
  private StringProperty rDelimiterBegan() {
320
    return mRDelimiterBegan;
321
  }
322
323
  public String getRDelimiterBegan() {
324
    return rDelimiterBegan().get();
325
  }
326
327
  private StringProperty rDelimiterEnded() {
328
    return mRDelimiterEnded;
329
  }
330
331
  public String getRDelimiterEnded() {
332
    return rDelimiterEnded().get();
333
  }
334
335
  private ObjectProperty<File> imagesDirectoryProperty() {
336
    return mPropImagesDirectory;
337
  }
338
339
  public File getImagesDirectory() {
340
    return imagesDirectoryProperty().getValue();
341
  }
342
343
  private StringProperty imagesOrderProperty() {
344
    return mPropImagesOrder;
345
  }
346
347
  public String getImagesOrder() {
348
    return imagesOrderProperty().getValue();
349
  }
350
351
  public IntegerProperty fontsSizeEditorProperty() {
352
    return mPropFontsSizeEditor;
353
  }
354
355
  /**
356
   * Returns the preferred font size of the text editor.
357
   *
358
   * @return A non-negative integer, in points.
359
   */
360
  public int getFontsSizeEditor() {
361
    return mPropFontsSizeEditor.intValue();
362
  }
363
364
  private PreferencesFx getPreferencesFx() {
365
    return mPreferencesFx;
366
  }
367
}
3681
D src/main/java/com/scrivenvar/preview/ChainedReplacedElementFactory.java
1
/*
2
 * Copyright 2006 Patrick Wright
3
 * Copyright 2007 Wisconsin Court System
4
 * Copyright 2020 White Magic Software, Ltd.
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU Lesser General Public License
8
 * as published by the Free Software Foundation; either version 2.1
9
 * of the License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19
 */
20
package com.scrivenvar.preview;
21
22
import com.scrivenvar.adapters.ReplacedElementAdapter;
23
import org.w3c.dom.Element;
24
import org.xhtmlrenderer.extend.ReplacedElement;
25
import org.xhtmlrenderer.extend.ReplacedElementFactory;
26
import org.xhtmlrenderer.extend.UserAgentCallback;
27
import org.xhtmlrenderer.layout.LayoutContext;
28
import org.xhtmlrenderer.render.BlockBox;
29
30
import java.util.HashSet;
31
import java.util.Set;
32
33
public class ChainedReplacedElementFactory extends ReplacedElementAdapter {
34
  private final Set<ReplacedElementFactory> mFactoryList = new HashSet<>();
35
36
  @Override
37
  public ReplacedElement createReplacedElement(
38
      final LayoutContext c,
39
      final BlockBox box,
40
      final UserAgentCallback uac,
41
      final int cssWidth,
42
      final int cssHeight ) {
43
    for( final var f : mFactoryList ) {
44
      final var r = f.createReplacedElement(
45
          c, box, uac, cssWidth, cssHeight );
46
47
      if( r != null ) {
48
        return r;
49
      }
50
    }
51
52
    return null;
53
  }
54
55
  @Override
56
  public void reset() {
57
    for( final var factory : mFactoryList ) {
58
      factory.reset();
59
    }
60
  }
61
62
  @Override
63
  public void remove( final Element element ) {
64
    for( final var factory : mFactoryList ) {
65
      factory.remove( element );
66
    }
67
  }
68
69
  public void addFactory( final ReplacedElementFactory factory ) {
70
    mFactoryList.add( factory );
71
  }
72
}
731
D src/main/java/com/scrivenvar/preview/CustomImageLoader.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import javafx.beans.property.IntegerProperty;
31
import javafx.beans.property.SimpleIntegerProperty;
32
import org.xhtmlrenderer.extend.FSImage;
33
import org.xhtmlrenderer.resource.ImageResource;
34
import org.xhtmlrenderer.swing.ImageResourceLoader;
35
36
import javax.imageio.ImageIO;
37
import java.net.URI;
38
import java.net.URL;
39
import java.nio.file.Paths;
40
41
import static com.scrivenvar.StatusBarNotifier.alert;
42
import static com.scrivenvar.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
43
import static com.scrivenvar.util.ProtocolResolver.getProtocol;
44
import static java.lang.String.valueOf;
45
import static java.nio.file.Files.exists;
46
import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
47
48
/**
49
 * Responsible for loading images. If the image cannot be found, a placeholder
50
 * is used instead.
51
 */
52
public class CustomImageLoader extends ImageResourceLoader {
53
  /**
54
   * Placeholder that's displayed when image cannot be found.
55
   */
56
  private FSImage mBrokenImage;
57
58
  private final IntegerProperty mWidthProperty = new SimpleIntegerProperty();
59
60
  /**
61
   * Gets an {@link IntegerProperty} that represents the maximum width an
62
   * image should be scaled.
63
   *
64
   * @return The maximum width for an image.
65
   */
66
  public IntegerProperty widthProperty() {
67
    return mWidthProperty;
68
  }
69
70
  /**
71
   * Gets an image resolved from the given URI. If the image cannot be found,
72
   * this will return a custom placeholder image indicating the reference
73
   * is broken.
74
   *
75
   * @param uri    Path to the image resource to load.
76
   * @param width  Ignored.
77
   * @param height Ignored.
78
   * @return The scaled image, or a placeholder image if the URI's content
79
   * could not be retrieved.
80
   */
81
  @Override
82
  public synchronized ImageResource get(
83
      final String uri, final int width, final int height ) {
84
    assert uri != null;
85
    assert width >= 0;
86
    assert height >= 0;
87
88
    try {
89
      final var protocol = getProtocol( uri );
90
      final ImageResource imageResource;
91
92
      if( protocol.isFile() && exists( Paths.get( new URI( uri ) ) ) ) {
93
        imageResource = super.get( uri, width, height );
94
      }
95
      else if( protocol.isHttp() ) {
96
        // FlyingSaucer will silently swallow any images that fail to load.
97
        // Consequently, the following lines load the resource over HTTP and
98
        // translate errors into a broken image icon.
99
        final var url = new URL( uri );
100
        final var image = ImageIO.read( url );
101
        imageResource = new ImageResource( uri, createImage( image ) );
102
      }
103
      else {
104
        // Caught below to return a broken image; exception is swallowed.
105
        throw new UnsupportedOperationException( valueOf( protocol ) );
106
      }
107
108
      return scale( imageResource );
109
    } catch( final Exception e ) {
110
      alert( e );
111
      return new ImageResource( uri, getBrokenImage() );
112
    }
113
  }
114
115
  /**
116
   * Scales the image found at the given URI.
117
   *
118
   * @param ir {@link ImageResource} of image loaded successfully.
119
   * @return Resource representing the rendered image and path.
120
   */
121
  private ImageResource scale( final ImageResource ir ) {
122
    final var image = ir.getImage();
123
    final var imageWidth = image.getWidth();
124
    final var imageHeight = image.getHeight();
125
126
    int maxWidth = mWidthProperty.get();
127
    int newWidth = imageWidth;
128
    int newHeight = imageHeight;
129
130
    // Maintain aspect ratio while shrinking image to view port bounds.
131
    if( imageWidth > maxWidth ) {
132
      newWidth = maxWidth;
133
      newHeight = (newWidth * imageHeight) / imageWidth;
134
    }
135
136
    image.scale( newWidth, newHeight );
137
    return ir;
138
  }
139
140
  /**
141
   * Lazily initializes the broken image placeholder.
142
   *
143
   * @return The {@link FSImage} that represents a broken image icon.
144
   */
145
  private FSImage getBrokenImage() {
146
    final var image = mBrokenImage;
147
148
    if( image == null ) {
149
      mBrokenImage = createImage( BROKEN_IMAGE_PLACEHOLDER );
150
    }
151
152
    return mBrokenImage;
153
  }
154
}
1551
D src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import com.scrivenvar.adapters.DocumentAdapter;
31
import javafx.beans.property.BooleanProperty;
32
import javafx.beans.property.SimpleBooleanProperty;
33
import javafx.beans.value.ChangeListener;
34
import javafx.beans.value.ObservableValue;
35
import javafx.embed.swing.SwingNode;
36
import javafx.scene.Node;
37
import org.jsoup.Jsoup;
38
import org.jsoup.helper.W3CDom;
39
import org.jsoup.nodes.Document;
40
import org.xhtmlrenderer.layout.SharedContext;
41
import org.xhtmlrenderer.render.Box;
42
import org.xhtmlrenderer.simple.XHTMLPanel;
43
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
44
import org.xhtmlrenderer.swing.*;
45
46
import javax.swing.*;
47
import java.awt.*;
48
import java.awt.event.ComponentAdapter;
49
import java.awt.event.ComponentEvent;
50
import java.net.URI;
51
import java.nio.file.Path;
52
53
import static com.scrivenvar.Constants.*;
54
import static com.scrivenvar.StatusBarNotifier.alert;
55
import static com.scrivenvar.util.ProtocolResolver.getProtocol;
56
import static java.awt.Desktop.Action.BROWSE;
57
import static java.awt.Desktop.getDesktop;
58
import static java.lang.Math.max;
59
import static javax.swing.SwingUtilities.invokeLater;
60
import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
61
62
/**
63
 * HTML preview pane is responsible for rendering an HTML document.
64
 */
65
public final class HTMLPreviewPane extends SwingNode {
66
67
  /**
68
   * Suppresses scrolling to the top on every key press.
69
   */
70
  private static class HTMLPanel extends XHTMLPanel {
71
    @Override
72
    public void resetScrollPosition() {
73
    }
74
  }
75
76
  /**
77
   * Suppresses scroll attempts until after the document has loaded.
78
   */
79
  private static final class DocumentEventHandler extends DocumentAdapter {
80
    private final BooleanProperty mReadyProperty = new SimpleBooleanProperty();
81
82
    public BooleanProperty readyProperty() {
83
      return mReadyProperty;
84
    }
85
86
    @Override
87
    public void documentStarted() {
88
      mReadyProperty.setValue( Boolean.FALSE );
89
    }
90
91
    @Override
92
    public void documentLoaded() {
93
      mReadyProperty.setValue( Boolean.TRUE );
94
    }
95
  }
96
97
  /**
98
   * Ensure that images are constrained to the panel width upon resizing.
99
   */
100
  private final class ResizeListener extends ComponentAdapter {
101
    @Override
102
    public void componentResized( final ComponentEvent e ) {
103
      setWidth( e );
104
    }
105
106
    @Override
107
    public void componentShown( final ComponentEvent e ) {
108
      setWidth( e );
109
    }
110
111
    /**
112
     * Sets the width of the {@link HTMLPreviewPane} so that images can be
113
     * scaled to fit. The scale factor is adjusted a bit below the full width
114
     * to prevent the horizontal scrollbar from appearing.
115
     *
116
     * @param event The component that defines the image scaling width.
117
     */
118
    private void setWidth( final ComponentEvent event ) {
119
      final int width = (int) (event.getComponent().getWidth() * .95);
120
      HTMLPreviewPane.this.mImageLoader.widthProperty().set( width );
121
    }
122
  }
123
124
  /**
125
   * Responsible for opening hyperlinks. External hyperlinks are opened in
126
   * the system's default browser; local file system links are opened in the
127
   * editor.
128
   */
129
  private static class HyperlinkListener extends LinkListener {
130
    @Override
131
    public void linkClicked( final BasicPanel panel, final String link ) {
132
      try {
133
        final var protocol = getProtocol( link );
134
135
        switch( protocol ) {
136
          case HTTP:
137
            final var desktop = getDesktop();
138
139
            if( desktop.isSupported( BROWSE ) ) {
140
              desktop.browse( new URI( link ) );
141
            }
142
            break;
143
          case FILE:
144
            // TODO: #88 -- publish a message to the event bus.
145
            break;
146
        }
147
      } catch( final Exception ex ) {
148
        alert( ex );
149
      }
150
    }
151
  }
152
153
  /**
154
   * The CSS must be rendered in points (pt) not pixels (px) to avoid blurry
155
   * rendering on some platforms.
156
   */
157
  private static final String HTML_PREFIX = "<!DOCTYPE html>"
158
      + "<html>"
159
      + "<head>"
160
      + "<link rel='stylesheet' href='" +
161
      HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) + "'/>"
162
      + "</head>"
163
      + "<body>";
164
165
  // Provide some extra space at the end for scrolling past the last line.
166
  private static final String HTML_SUFFIX =
167
      "<p style='height=2em'>&nbsp;</p></body></html>";
168
169
  private static final W3CDom W3C_DOM = new W3CDom();
170
  private static final XhtmlNamespaceHandler NS_HANDLER =
171
      new XhtmlNamespaceHandler();
172
173
  private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
174
  private final int mHtmlPrefixLength;
175
176
  private final HTMLPanel mHtmlRenderer = new HTMLPanel();
177
  private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
178
  private final DocumentEventHandler mDocHandler = new DocumentEventHandler();
179
  private final CustomImageLoader mImageLoader = new CustomImageLoader();
180
181
  private Path mPath = DEFAULT_DIRECTORY;
182
183
  /**
184
   * Creates a new preview pane that can scroll to the caret position within the
185
   * document.
186
   */
187
  public HTMLPreviewPane() {
188
    setStyle( "-fx-background-color: white;" );
189
190
    // No need to append same prefix each time the HTML content is updated.
191
    mHtmlDocument.append( HTML_PREFIX );
192
    mHtmlPrefixLength = mHtmlDocument.length();
193
194
    // Inject an SVG renderer that produces high-quality SVG buffered images.
195
    final var factory = new ChainedReplacedElementFactory();
196
    factory.addFactory( new SvgReplacedElementFactory() );
197
    factory.addFactory( new SwingReplacedElementFactory(
198
        NO_OP_REPAINT_LISTENER, mImageLoader ) );
199
200
    final var context = getSharedContext();
201
    final var textRenderer = context.getTextRenderer();
202
    context.setReplacedElementFactory( factory );
203
    textRenderer.setSmoothingThreshold( 0 );
204
205
    setContent( mScrollPane );
206
    mHtmlRenderer.addDocumentListener( mDocHandler );
207
    mHtmlRenderer.addComponentListener( new ResizeListener() );
208
209
    // The default mouse click listener attempts navigation within the
210
    // preview panel. We want to usurp that behaviour to open the link in
211
    // a platform-specific browser.
212
    for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) {
213
      if( !(listener instanceof HoverListener) ) {
214
        mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener );
215
      }
216
    }
217
218
    mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() );
219
  }
220
221
  /**
222
   * Updates the internal HTML source, loads it into the preview pane, then
223
   * scrolls to the caret position.
224
   *
225
   * @param html The new HTML document to display.
226
   */
227
  public void process( final String html ) {
228
    final Document jsoupDoc = Jsoup.parse( decorate( html ) );
229
    final org.w3c.dom.Document w3cDoc = W3C_DOM.fromJsoup( jsoupDoc );
230
231
232
    // Access to a Swing component must occur from the Event Dispatch
233
    // thread according to Swing threading restrictions.
234
    invokeLater(
235
        () -> mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), NS_HANDLER )
236
    );
237
  }
238
239
  public void clear() {
240
    process( "" );
241
  }
242
243
  /**
244
   * Scrolls to an anchor link. The anchor links are injected when the
245
   * HTML document is created.
246
   *
247
   * @param id The unique anchor link identifier.
248
   */
249
  public void tryScrollTo( final int id ) {
250
    final ChangeListener<Boolean> listener = new ChangeListener<>() {
251
      @Override
252
      public void changed(
253
          final ObservableValue<? extends Boolean> observable,
254
          final Boolean oldValue,
255
          final Boolean newValue ) {
256
        if( newValue ) {
257
          scrollTo( id );
258
259
          mDocHandler.readyProperty().removeListener( this );
260
        }
261
      }
262
    };
263
264
    mDocHandler.readyProperty().addListener( listener );
265
  }
266
267
  /**
268
   * Scrolls to the closest element matching the given identifier without
269
   * waiting for the document to be ready. Be sure the document is ready
270
   * before calling this method.
271
   *
272
   * @param id Paragraph index.
273
   */
274
  public void scrollTo( final int id ) {
275
    if( id < 2 ) {
276
      scrollToTop();
277
    }
278
    else {
279
      Box box = findPrevBox( id );
280
      box = box == null ? findNextBox( id + 1 ) : box;
281
282
      if( box == null ) {
283
        scrollToBottom();
284
      }
285
      else {
286
        scrollTo( box );
287
      }
288
    }
289
  }
290
291
  private Box findPrevBox( final int id ) {
292
    int prevId = id;
293
    Box box = null;
294
295
    while( prevId > 0 && (box = getBoxById( PARAGRAPH_ID_PREFIX + prevId )) == null ) {
296
      prevId--;
297
    }
298
299
    return box;
300
  }
301
302
  private Box findNextBox( final int id ) {
303
    int nextId = id;
304
    Box box = null;
305
306
    while( nextId - id < 5 &&
307
        (box = getBoxById( PARAGRAPH_ID_PREFIX + nextId )) == null ) {
308
      nextId++;
309
    }
310
311
    return box;
312
  }
313
314
  private void scrollTo( final Point point ) {
315
    invokeLater( () -> mHtmlRenderer.scrollTo( point ) );
316
  }
317
318
  private void scrollTo( final Box box ) {
319
    scrollTo( createPoint( box ) );
320
  }
321
322
  private void scrollToY( final int y ) {
323
    scrollTo( new Point( 0, y ) );
324
  }
325
326
  private void scrollToTop() {
327
    scrollToY( 0 );
328
  }
329
330
  private void scrollToBottom() {
331
    scrollToY( mHtmlRenderer.getHeight() );
332
  }
333
334
  private Box getBoxById( final String id ) {
335
    return getSharedContext().getBoxById( id );
336
  }
337
338
  private String decorate( final String html ) {
339
    // Trim the HTML back to only the prefix.
340
    mHtmlDocument.setLength( mHtmlPrefixLength );
341
342
    // Write the HTML body element followed by closing tags.
343
    return mHtmlDocument.append( html ).append( HTML_SUFFIX ).toString();
344
  }
345
346
  public Path getPath() {
347
    return mPath;
348
  }
349
350
  public void setPath( final Path path ) {
351
    assert path != null;
352
    mPath = path;
353
  }
354
355
  /**
356
   * Content to embed in a panel.
357
   *
358
   * @return The content to display to the user.
359
   */
360
  public Node getNode() {
361
    return this;
362
  }
363
364
  public JScrollPane getScrollPane() {
365
    return mScrollPane;
366
  }
367
368
  public JScrollBar getVerticalScrollBar() {
369
    return getScrollPane().getVerticalScrollBar();
370
  }
371
372
  /**
373
   * Creates a {@link Point} to use as a reference for scrolling to the area
374
   * described by the given {@link Box}. The {@link Box} coordinates are used
375
   * to populate the {@link Point}'s location, with minor adjustments for
376
   * vertical centering.
377
   *
378
   * @param box The {@link Box} that represents a scrolling anchor reference.
379
   * @return A coordinate suitable for scrolling to.
380
   */
381
  private Point createPoint( final Box box ) {
382
    assert box != null;
383
384
    int x = box.getAbsX();
385
386
    // Scroll back up by half the height of the scroll bar to keep the typing
387
    // area within the view port. Otherwise the view port will have jumped too
388
    // high up and the whatever gets typed won't be visible.
389
    int y = max(
390
        box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2),
391
        0 );
392
393
    if( !box.getStyle().isInline() ) {
394
      final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() );
395
      x += margin.left();
396
      y += margin.top();
397
    }
398
399
    return new Point( x, y );
400
  }
401
402
  private String getBaseUrl() {
403
    final Path basePath = getPath();
404
    final Path parent = basePath == null ? null : basePath.getParent();
405
406
    return parent == null ? "" : parent.toUri().toString();
407
  }
408
409
  private SharedContext getSharedContext() {
410
    return mHtmlRenderer.getSharedContext();
411
  }
412
}
4131
D src/main/java/com/scrivenvar/preview/MathRenderer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import com.scrivenvar.preferences.UserPreferences;
31
import com.whitemagicsoftware.tex.*;
32
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
33
import javafx.beans.property.IntegerProperty;
34
import org.w3c.dom.Document;
35
36
import java.util.function.Supplier;
37
38
import static com.scrivenvar.StatusBarNotifier.alert;
39
40
/**
41
 * Responsible for rendering formulas as scalable vector graphics (SVG).
42
 */
43
public class MathRenderer {
44
45
  /**
46
   * Default font size in points.
47
   */
48
  private static final float FONT_SIZE = 20f;
49
50
  private final TeXFont mTeXFont = createDefaultTeXFont( FONT_SIZE );
51
  private final TeXEnvironment mEnvironment = createTeXEnvironment( mTeXFont );
52
  private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D();
53
54
  public MathRenderer() {
55
    mGraphics.scale( FONT_SIZE, FONT_SIZE );
56
  }
57
58
  /**
59
   * This method only takes a few seconds to generate
60
   *
61
   * @param equation A mathematical expression to render.
62
   * @return The given string with all formulas transformed into SVG format.
63
   */
64
  public Document render( final String equation ) {
65
    final var formula = new TeXFormula( equation );
66
    final var box = formula.createBox( mEnvironment );
67
    final var l = new TeXLayout( box, FONT_SIZE );
68
69
    mGraphics.initialize( l.getWidth(), l.getHeight() );
70
    box.draw( mGraphics, l.getX(), l.getY() );
71
    return mGraphics.toDom();
72
  }
73
74
  @SuppressWarnings("SameParameterValue")
75
  private TeXFont createDefaultTeXFont( final float fontSize ) {
76
    return create( () -> new DefaultTeXFont( fontSize ) );
77
  }
78
79
  private TeXEnvironment createTeXEnvironment( final TeXFont texFont ) {
80
    return create( () -> new TeXEnvironment( texFont ) );
81
  }
82
83
  private SvgDomGraphics2D createSvgDomGraphics2D() {
84
    return create( SvgDomGraphics2D::new );
85
  }
86
87
  /**
88
   * Tries to instantiate a given object, returning {@code null} on failure.
89
   * The failure message is bubbled up to to the user interface.
90
   *
91
   * @param supplier Creates an instance.
92
   * @param <T>      The type of instance being created.
93
   * @return An instance of the parameterized type or {@code null} upon error.
94
   */
95
  private <T> T create( final Supplier<T> supplier ) {
96
    try {
97
      return supplier.get();
98
    } catch( final Exception ex ) {
99
      alert( ex );
100
      return null;
101
    }
102
  }
103
}
1041
D src/main/java/com/scrivenvar/preview/RenderingSettings.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
33
import static java.awt.RenderingHints.*;
34
import static java.awt.Toolkit.getDefaultToolkit;
35
36
/**
37
 * Responsible for supplying consistent rendering hints throughout the
38
 * application, such as image rendering for {@link SvgRasterizer}.
39
 */
40
@SuppressWarnings("rawtypes")
41
public class RenderingSettings {
42
43
  /**
44
   * Default hints for high-quality rendering that may be changed by
45
   * the system's rendering hints.
46
   */
47
  private static final Map<Object, Object> DEFAULT_HINTS = Map.of(
48
      KEY_ANTIALIASING,
49
      VALUE_ANTIALIAS_ON,
50
      KEY_ALPHA_INTERPOLATION,
51
      VALUE_ALPHA_INTERPOLATION_QUALITY,
52
      KEY_COLOR_RENDERING,
53
      VALUE_COLOR_RENDER_QUALITY,
54
      KEY_DITHERING,
55
      VALUE_DITHER_DISABLE,
56
      KEY_FRACTIONALMETRICS,
57
      VALUE_FRACTIONALMETRICS_ON,
58
      KEY_INTERPOLATION,
59
      VALUE_INTERPOLATION_BICUBIC,
60
      KEY_RENDERING,
61
      VALUE_RENDER_QUALITY,
62
      KEY_STROKE_CONTROL,
63
      VALUE_STROKE_PURE,
64
      KEY_TEXT_ANTIALIASING,
65
      VALUE_TEXT_ANTIALIAS_ON
66
  );
67
68
  /**
69
   * Shared hints for high-quality rendering.
70
   */
71
  public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>(
72
      DEFAULT_HINTS
73
  );
74
75
  static {
76
    final var toolkit = getDefaultToolkit();
77
    final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" );
78
79
    if( hints instanceof Map ) {
80
      final var map = (Map) hints;
81
      for( final var key : map.keySet() ) {
82
        final var hint = map.get( key );
83
        RENDERING_HINTS.put( key, hint );
84
      }
85
    }
86
  }
87
88
  /**
89
   * Prevent instantiation as per Joshua Bloch's recommendation.
90
   */
91
  private RenderingSettings() {
92
  }
93
}
941
D src/main/java/com/scrivenvar/preview/SvgRasterizer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
31
import org.apache.batik.gvt.renderer.ImageRenderer;
32
import org.apache.batik.transcoder.TranscoderException;
33
import org.apache.batik.transcoder.TranscoderInput;
34
import org.apache.batik.transcoder.TranscoderOutput;
35
import org.apache.batik.transcoder.image.ImageTranscoder;
36
import org.w3c.dom.Document;
37
import org.w3c.dom.Element;
38
39
import javax.xml.transform.Transformer;
40
import javax.xml.transform.TransformerConfigurationException;
41
import javax.xml.transform.TransformerFactory;
42
import javax.xml.transform.dom.DOMSource;
43
import javax.xml.transform.stream.StreamResult;
44
import java.awt.*;
45
import java.awt.image.BufferedImage;
46
import java.io.IOException;
47
import java.io.StringReader;
48
import java.io.StringWriter;
49
import java.net.URL;
50
import java.text.NumberFormat;
51
52
import static com.scrivenvar.StatusBarNotifier.alert;
53
import static com.scrivenvar.preview.RenderingSettings.RENDERING_HINTS;
54
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
55
import static java.nio.charset.StandardCharsets.UTF_8;
56
import static java.text.NumberFormat.getIntegerInstance;
57
import static javax.xml.transform.OutputKeys.*;
58
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
59
import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
60
61
/**
62
 * Responsible for converting SVG images into rasterized PNG images.
63
 */
64
public class SvgRasterizer {
65
  private static final SAXSVGDocumentFactory FACTORY_DOM =
66
      new SAXSVGDocumentFactory( getXMLParserClassName() );
67
68
  private static final TransformerFactory FACTORY_TRANSFORM =
69
      TransformerFactory.newInstance();
70
71
  private static final Transformer sTransformer;
72
73
  static {
74
    Transformer t;
75
76
    try {
77
      t = FACTORY_TRANSFORM.newTransformer();
78
      t.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
79
      t.setOutputProperty( METHOD, "xml" );
80
      t.setOutputProperty( INDENT, "no" );
81
      t.setOutputProperty( ENCODING, UTF_8.name() );
82
    } catch( final TransformerConfigurationException e ) {
83
      t = null;
84
    }
85
86
    sTransformer = t;
87
  }
88
89
  private static final NumberFormat INT_FORMAT = getIntegerInstance();
90
91
  public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER;
92
93
  /**
94
   * A FontAwesome camera icon, cleft asunder.
95
   */
96
  public static final String BROKEN_IMAGE_SVG =
97
      "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" +
98
          ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" +
99
          ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " +
100
          "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" +
101
          ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" +
102
          ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" +
103
          ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" +
104
          ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " +
105
          "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" +
106
          ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" +
107
          ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" +
108
          ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" +
109
          ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" +
110
          ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" +
111
          ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" +
112
          ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" +
113
          ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " +
114
          ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" +
115
          ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" +
116
          ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" +
117
          ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" +
118
          ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" +
119
          ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" +
120
          ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " +
121
          "0'/></g></svg>";
122
123
  static {
124
    // The width and height cannot be embedded in the SVG above because the
125
    // path element values are relative to the viewBox dimensions.
126
    final int w = 75;
127
    final int h = 75;
128
    BufferedImage image;
129
130
    try {
131
      image = rasterizeString( BROKEN_IMAGE_SVG, w );
132
    } catch( final Exception e ) {
133
      image = new BufferedImage( w, h, TYPE_INT_RGB );
134
      final var graphics = (Graphics2D) image.getGraphics();
135
      graphics.setRenderingHints( RENDERING_HINTS );
136
137
      // Fall back to a (\) symbol.
138
      graphics.setColor( new Color( 204, 204, 204 ) );
139
      graphics.fillRect( 0, 0, w, h );
140
      graphics.setColor( new Color( 255, 204, 204 ) );
141
      graphics.setStroke( new BasicStroke( 4 ) );
142
      graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
143
      graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
144
                         h / 4 + (int) (w / 4 / Math.PI),
145
                         w / 2 + w / 4 - (int) (w / 4 / Math.PI),
146
                         h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
147
    }
148
149
    BROKEN_IMAGE_PLACEHOLDER = image;
150
  }
151
152
  /**
153
   * Responsible for creating a new {@link ImageRenderer} implementation that
154
   * can render a DOM as an SVG image.
155
   */
156
  private static class BufferedImageTranscoder extends ImageTranscoder {
157
    private BufferedImage mImage;
158
159
    @Override
160
    public BufferedImage createImage( final int w, final int h ) {
161
      return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
162
    }
163
164
    @Override
165
    public void writeImage(
166
        final BufferedImage image, final TranscoderOutput output ) {
167
      mImage = image;
168
    }
169
170
    public BufferedImage getImage() {
171
      return mImage;
172
    }
173
174
    @Override
175
    protected ImageRenderer createRenderer() {
176
      final ImageRenderer renderer = super.createRenderer();
177
      final RenderingHints hints = renderer.getRenderingHints();
178
      hints.putAll( RENDERING_HINTS );
179
180
      renderer.setRenderingHints( hints );
181
182
      return renderer;
183
    }
184
  }
185
186
  /**
187
   * Rasterizes the vector graphic file at the given URL. If any exception
188
   * happens, a red circle is returned instead.
189
   *
190
   * @param url   The URL to a vector graphic file, which must include the
191
   *              protocol scheme (such as file:// or https://).
192
   * @param width The number of pixels wide to render the image. The aspect
193
   *              ratio is maintained.
194
   * @return Either the rasterized image upon success or a red circle.
195
   */
196
  public static BufferedImage rasterize( final String url, final int width ) {
197
    try {
198
      return rasterize( new URL( url ), width );
199
    } catch( final Exception ex ) {
200
      alert( ex );
201
      return BROKEN_IMAGE_PLACEHOLDER;
202
    }
203
  }
204
205
  /**
206
   * Rasterizes the given document into an image.
207
   *
208
   * @param svg   The SVG {@link Document} to rasterize.
209
   * @param width The rasterized image's width (in pixels).
210
   * @return The rasterized image.
211
   * @throws TranscoderException Signifies an issue with the input document.
212
   */
213
  public static BufferedImage rasterize( final Document svg, final int width )
214
      throws TranscoderException {
215
    final var transcoder = new BufferedImageTranscoder();
216
    final var input = new TranscoderInput( svg );
217
218
    transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
219
    transcoder.transcode( input, null );
220
221
    return transcoder.getImage();
222
  }
223
224
  /**
225
   * Converts an SVG drawing into a rasterized image that can be drawn on
226
   * a graphics context.
227
   *
228
   * @param url   The path to the image (can be web address).
229
   * @param width Scale the image width to this size (aspect ratio is
230
   *              maintained).
231
   * @return The vector graphic transcoded into a raster image format.
232
   * @throws IOException         Could not read the vector graphic.
233
   * @throws TranscoderException Could not convert the vector graphic to an
234
   *                             instance of {@link Image}.
235
   */
236
  public static BufferedImage rasterize( final URL url, final int width )
237
      throws IOException, TranscoderException {
238
    return rasterize( FACTORY_DOM.createDocument( url.toString() ), width );
239
  }
240
241
  public static BufferedImage rasterize( final Document document ) {
242
    try {
243
      final var root = document.getDocumentElement();
244
      final var width = root.getAttribute( "width" );
245
      return rasterize( document, INT_FORMAT.parse( width ).intValue() );
246
    } catch( final Exception ex ) {
247
      alert( ex );
248
      return BROKEN_IMAGE_PLACEHOLDER;
249
    }
250
  }
251
252
  /**
253
   * Converts an SVG string into a rasterized image that can be drawn on
254
   * a graphics context.
255
   *
256
   * @param svg The SVG xml document.
257
   * @param w   Scale the image width to this size (aspect ratio is
258
   *            maintained).
259
   * @return The vector graphic transcoded into a raster image format.
260
   * @throws TranscoderException Could not convert the vector graphic to an
261
   *                             instance of {@link Image}.
262
   */
263
  public static BufferedImage rasterizeString( final String svg, final int w )
264
      throws IOException, TranscoderException {
265
    return rasterize( toDocument( svg ), w );
266
  }
267
268
  /**
269
   * Converts an SVG string into a rasterized image that can be drawn on
270
   * a graphics context. The dimensions are determined from the document.
271
   *
272
   * @param xml The SVG xml document.
273
   * @return The vector graphic transcoded into a raster image format.
274
   */
275
  public static BufferedImage rasterizeString( final String xml ) {
276
    try {
277
      final var document = toDocument( xml );
278
      final var root = document.getDocumentElement();
279
      final var width = root.getAttribute( "width" );
280
      return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() );
281
    } catch( final Exception ex ) {
282
      alert( ex );
283
      return BROKEN_IMAGE_PLACEHOLDER;
284
    }
285
  }
286
287
  /**
288
   * Converts an SVG XML string into a new {@link Document} instance.
289
   *
290
   * @param xml The XML containing SVG elements.
291
   * @return The SVG contents parsed into a {@link Document} object model.
292
   * @throws IOException Could
293
   */
294
  private static Document toDocument( final String xml ) throws IOException {
295
    try( final var reader = new StringReader( xml ) ) {
296
      return FACTORY_DOM.createSVGDocument(
297
          "http://www.w3.org/2000/svg", reader );
298
    }
299
  }
300
301
  /**
302
   * Given a document object model (DOM) {@link Element}, this will convert that
303
   * element to a string.
304
   *
305
   * @param e The DOM node to convert to a string.
306
   * @return The DOM node as an escaped, plain text string.
307
   */
308
  public static String toSvg( final Element e ) {
309
    try( final var writer = new StringWriter() ) {
310
      sTransformer.transform( new DOMSource( e ), new StreamResult( writer ) );
311
      return writer.toString().replaceAll( "xmlns=\"\" ", "" );
312
    } catch( final Exception ex ) {
313
      alert( ex );
314
    }
315
316
    return BROKEN_IMAGE_SVG;
317
  }
318
}
3191
D src/main/java/com/scrivenvar/preview/SvgReplacedElementFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import com.scrivenvar.util.BoundedCache;
31
import org.apache.commons.io.FilenameUtils;
32
import org.w3c.dom.Element;
33
import org.xhtmlrenderer.extend.ReplacedElement;
34
import org.xhtmlrenderer.extend.ReplacedElementFactory;
35
import org.xhtmlrenderer.extend.UserAgentCallback;
36
import org.xhtmlrenderer.layout.LayoutContext;
37
import org.xhtmlrenderer.render.BlockBox;
38
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
39
import org.xhtmlrenderer.swing.ImageReplacedElement;
40
41
import java.awt.image.BufferedImage;
42
import java.util.Map;
43
import java.util.function.Function;
44
45
import static com.scrivenvar.StatusBarNotifier.alert;
46
import static com.scrivenvar.preview.SvgRasterizer.rasterize;
47
import static com.scrivenvar.processors.markdown.tex.TeXNode.HTML_TEX;
48
49
/**
50
 * Responsible for running {@link SvgRasterizer} on SVG images detected within
51
 * a document to transform them into rasterized versions.
52
 */
53
public class SvgReplacedElementFactory implements ReplacedElementFactory {
54
55
  /**
56
   * Prevent instantiation until needed.
57
   */
58
  private static class MathRendererContainer {
59
    private static final MathRenderer INSTANCE = new MathRenderer();
60
  }
61
62
  /**
63
   * Returns the singleton instance for rendering math symbols.
64
   *
65
   * @return A non-null instance, loaded, configured, and ready to render math.
66
   */
67
  public static MathRenderer getInstance() {
68
    return MathRendererContainer.INSTANCE;
69
  }
70
71
  /**
72
   * SVG filename extension maps to an SVG image element.
73
   */
74
  private static final String SVG_FILE = "svg";
75
76
  private static final String HTML_IMAGE = "img";
77
  private static final String HTML_IMAGE_SRC = "src";
78
79
  /**
80
   * A bounded cache that removes the oldest image if the maximum number of
81
   * cached images has been reached. This constrains the number of images
82
   * loaded into memory.
83
   */
84
  private final Map<String, BufferedImage> mImageCache =
85
      new BoundedCache<>( 150 );
86
87
  @Override
88
  public ReplacedElement createReplacedElement(
89
      final LayoutContext c,
90
      final BlockBox box,
91
      final UserAgentCallback uac,
92
      final int cssWidth,
93
      final int cssHeight ) {
94
    BufferedImage image = null;
95
    final var e = box.getElement();
96
97
    if( e != null ) {
98
      try {
99
        final var nodeName = e.getNodeName();
100
101
        if( HTML_IMAGE.equals( nodeName ) ) {
102
          final var src = e.getAttribute( HTML_IMAGE_SRC );
103
          final var ext = FilenameUtils.getExtension( src );
104
105
          if( SVG_FILE.equalsIgnoreCase( ext ) ) {
106
            image = getCachedImage(
107
                src, svg -> rasterize( svg, box.getContentWidth() ) );
108
          }
109
        }
110
        else if( HTML_TEX.equals( nodeName ) ) {
111
          // Convert the TeX element to a raster graphic if not yet cached.
112
          final var src = e.getTextContent();
113
          image = getCachedImage(
114
              src, __ -> rasterize( getInstance().render( src ) )
115
          );
116
        }
117
      } catch( final Exception ex ) {
118
        alert( ex );
119
      }
120
    }
121
122
    if( image != null ) {
123
      final var w = image.getWidth( null );
124
      final var h = image.getHeight( null );
125
126
      return new ImageReplacedElement( image, w, h );
127
    }
128
129
    return null;
130
  }
131
132
  @Override
133
  public void reset() {
134
  }
135
136
  @Override
137
  public void remove( final Element e ) {
138
  }
139
140
  @Override
141
  public void setFormSubmissionListener( FormSubmissionListener listener ) {
142
  }
143
144
  /**
145
   * Returns an image associated with a string; the string's pre-computed
146
   * hash code is returned as the string value, making this operation very
147
   * quick to return the corresponding {@link BufferedImage}.
148
   *
149
   * @param src        The source used for the key into the image cache.
150
   * @param rasterizer {@link Function} to call to rasterize an image.
151
   * @return The image that corresponds to the given source string.
152
   */
153
  private BufferedImage getCachedImage(
154
      final String src, final Function<String, BufferedImage> rasterizer ) {
155
    return mImageCache.computeIfAbsent( src, __ -> rasterizer.apply( src ) );
156
  }
157
}
1581
D src/main/java/com/scrivenvar/processors/AbstractProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * Responsible for transforming a document through a variety of chained
32
 * handlers. If there are conditions where this handler should not process the
33
 * entire chain, create a second handler, or split the chain into reusable
34
 * sub-chains.
35
 *
36
 * @param <T> The type of object to process.
37
 */
38
public abstract class AbstractProcessor<T> implements Processor<T> {
39
40
  /**
41
   * Used while processing the entire chain; null to signify no more links.
42
   */
43
  private final Processor<T> mNext;
44
45
  /**
46
   * Constructs a new default handler with no successor.
47
   */
48
  protected AbstractProcessor() {
49
    this( null );
50
  }
51
52
  /**
53
   * Constructs a new default handler with a given successor.
54
   *
55
   * @param successor The next processor in the chain.
56
   */
57
  public AbstractProcessor( final Processor<T> successor ) {
58
    mNext = successor;
59
  }
60
61
  @Override
62
  public Processor<T> next() {
63
    return mNext;
64
  }
65
66
  /**
67
   * This algorithm is incorrect, but works for the one use case of removing
68
   * the ending HTML Preview Processor from the end of the processor chain.
69
   * The processor chain is immutable so this creates a succession of
70
   * delegators that wrap each processor in the chain, except for the one
71
   * to be removed.
72
   * <p>
73
   * An alternative is to update the {@link ProcessorFactory} with the ability
74
   * to create a processor chain devoid of an {@link HtmlPreviewProcessor}.
75
   * </p>
76
   *
77
   * @param removal The {@link Processor} to remove from the chain.
78
   * @return A delegating processor chain starting from this processor
79
   * onwards with the given processor removed from the chain.
80
   */
81
  @Override
82
  public Processor<T> remove( final Class<? extends Processor<T>> removal ) {
83
    Processor<T> p = this;
84
    final ProcessorDelegator<T> head = new ProcessorDelegator<>( p );
85
    ProcessorDelegator<T> result = head;
86
87
    while( p != null ) {
88
      final Processor<T> next = p.next();
89
90
      if( next != null && next.getClass() != removal ) {
91
        final var delegator = new ProcessorDelegator<>( next );
92
93
        result.setNext( delegator );
94
        result = delegator;
95
      }
96
97
      p = p.next();
98
    }
99
100
    return head;
101
  }
102
103
  private static final class ProcessorDelegator<T>
104
      extends AbstractProcessor<T> {
105
    private final Processor<T> mDelegate;
106
    private Processor<T> mNext;
107
108
    public ProcessorDelegator( final Processor<T> delegate ) {
109
      super( delegate );
110
111
      assert delegate != null;
112
113
      mDelegate = delegate;
114
    }
115
116
    @Override
117
    public T apply( T t ) {
118
      return mDelegate.apply( t );
119
    }
120
121
    protected void setNext( final Processor<T> next ) {
122
      mNext = next;
123
    }
124
125
    @Override
126
    public Processor<T> next() {
127
      return mNext;
128
    }
129
  }
130
}
1311
D src/main/java/com/scrivenvar/processors/DefinitionProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import java.util.Map;
31
32
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
33
34
/**
35
 * Processes interpolated string definitions in the document and inserts
36
 * their values into the post-processed text. The default variable syntax is
37
 * {@code $variable$}.
38
 */
39
public class DefinitionProcessor extends AbstractProcessor<String> {
40
41
  private final Map<String, String> mDefinitions;
42
43
  public DefinitionProcessor(
44
      final Processor<String> successor, final Map<String, String> map ) {
45
    super( successor );
46
    mDefinitions = map;
47
  }
48
49
  /**
50
   * Processes the given text document by replacing variables with their values.
51
   *
52
   * @param text The document text that includes variables that should be
53
   *             replaced with values when rendered as HTML.
54
   * @return The text with all variables replaced.
55
   */
56
  @Override
57
  public String apply( final String text ) {
58
    return replace( text, getDefinitions() );
59
  }
60
61
  /**
62
   * Returns the map to use for variable substitution.
63
   *
64
   * @return A map of variable names to values.
65
   */
66
  protected Map<String, String> getDefinitions() {
67
    return mDefinitions;
68
  }
69
}
701
D src/main/java/com/scrivenvar/processors/HtmlPreviewProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.preview.HTMLPreviewPane;
31
32
/**
33
 * Responsible for notifying the HTMLPreviewPane when the succession chain has
34
 * updated. This decouples knowledge of changes to the editor panel from the
35
 * HTML preview panel as well as any processing that takes place before the
36
 * final HTML preview is rendered. This should be the last link in the processor
37
 * chain.
38
 */
39
public class HtmlPreviewProcessor extends AbstractProcessor<String> {
40
41
  // There is only one preview panel.
42
  private static HTMLPreviewPane sHtmlPreviewPane;
43
44
  /**
45
   * Constructs the end of a processing chain.
46
   *
47
   * @param htmlPreviewPane The pane to update with the post-processed document.
48
   */
49
  public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
50
    sHtmlPreviewPane = htmlPreviewPane;
51
  }
52
53
  /**
54
   * Update the preview panel using HTML from the succession chain.
55
   *
56
   * @param html The document content to render in the preview pane. The HTML
57
   *             should not contain a doctype, head, or body tag, only
58
   *             content to render within the body.
59
   * @return {@code null} to indicate no more processors in the chain.
60
   */
61
  @Override
62
  public String apply( final String html ) {
63
    getHtmlPreviewPane().process( html );
64
65
    // No more processing required.
66
    return null;
67
  }
68
69
  private HTMLPreviewPane getHtmlPreviewPane() {
70
    return sHtmlPreviewPane;
71
  }
72
}
731
D src/main/java/com/scrivenvar/processors/IdentityProcessor.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * This is the default processor used when an unknown filename extension is
32
 * encountered.
33
 */
34
public class IdentityProcessor extends AbstractProcessor<String> {
35
36
  /**
37
   * Passes the link to the super constructor.
38
   *
39
   * @param successor The next processor in the chain to use for text
40
   *                  processing.
41
   */
42
  public IdentityProcessor( final Processor<String> successor ) {
43
    super( successor );
44
  }
45
46
  /**
47
   * Returns the given string, modified with "pre" tags.
48
   *
49
   * @param t The string to return, enclosed in "pre" tags.
50
   * @return The value of t wrapped in "pre" tags.
51
   */
52
  @Override
53
  public String apply( final String t ) {
54
    return "<pre>" + t + "</pre>";
55
  }
56
}
571
D src/main/java/com/scrivenvar/processors/InlineRProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.preferences.UserPreferences;
31
import javafx.beans.property.ObjectProperty;
32
import javafx.beans.property.StringProperty;
33
34
import javax.script.ScriptEngine;
35
import javax.script.ScriptEngineManager;
36
import java.io.File;
37
import java.nio.file.Path;
38
import java.util.LinkedHashMap;
39
import java.util.Map;
40
import java.util.concurrent.atomic.AtomicBoolean;
41
42
import static com.scrivenvar.Constants.STATUS_PARSE_ERROR;
43
import static com.scrivenvar.StatusBarNotifier.alert;
44
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
45
import static com.scrivenvar.sigils.RSigilOperator.PREFIX;
46
import static com.scrivenvar.sigils.RSigilOperator.SUFFIX;
47
import static java.lang.Math.min;
48
49
/**
50
 * Transforms a document containing R statements into Markdown.
51
 */
52
public final class InlineRProcessor extends DefinitionProcessor {
53
  /**
54
   * Constrain memory when typing new R expressions into the document.
55
   */
56
  private static final int MAX_CACHED_R_STATEMENTS = 512;
57
58
  /**
59
   * Where to put document inline evaluated R expressions.
60
   */
61
  private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
62
    @Override
63
    protected boolean removeEldestEntry(
64
        final Map.Entry<String, Object> eldest ) {
65
      return size() > MAX_CACHED_R_STATEMENTS;
66
    }
67
  };
68
69
  /**
70
   * Only one editor is open at a time.
71
   */
72
  private static final ScriptEngine ENGINE =
73
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
74
75
  private static final int PREFIX_LENGTH = PREFIX.length();
76
77
  private final AtomicBoolean mDirty = new AtomicBoolean( false );
78
79
  /**
80
   * Constructs a processor capable of evaluating R statements.
81
   *
82
   * @param successor Subsequent link in the processing chain.
83
   * @param map       Resolved definitions map.
84
   */
85
  public InlineRProcessor(
86
      final Processor<String> successor,
87
      final Map<String, String> map ) {
88
    super( successor, map );
89
90
    bootstrapScriptProperty().addListener(
91
        ( ob, oldScript, newScript ) -> setDirty( true ) );
92
    workingDirectoryProperty().addListener(
93
        ( ob, oldScript, newScript ) -> setDirty( true ) );
94
95
    getUserPreferences().addSaveEventHandler( ( handler ) -> {
96
      if( isDirty() ) {
97
        init();
98
        setDirty( false );
99
      }
100
    } );
101
102
    init();
103
  }
104
105
  /**
106
   * Initialises the R code so that R can find imported libraries. Note that
107
   * any existing R functionality will not be overwritten if this method is
108
   * called multiple times.
109
   */
110
  private void init() {
111
    final var bootstrap = getBootstrapScript();
112
113
    if( !bootstrap.isBlank() ) {
114
      final var wd = getWorkingDirectory();
115
      final var dir = wd.toString().replace( '\\', '/' );
116
      final var map = getDefinitions();
117
      map.put( "$application.r.working.directory$", dir );
118
119
      eval( replace( bootstrap, map ) );
120
    }
121
  }
122
123
  /**
124
   * Sets the dirty flag to indicate that the bootstrap script or working
125
   * directory has been modified. Upon saving the preferences, if this flag
126
   * is true, then {@link #init()} will be called to reload the R environment.
127
   *
128
   * @param dirty Set to true to reload changes upon closing preferences.
129
   */
130
  private void setDirty( final boolean dirty ) {
131
    mDirty.set( dirty );
132
  }
133
134
  /**
135
   * Answers whether R-related settings have been modified.
136
   *
137
   * @return {@code true} when the settings have changed.
138
   */
139
  private boolean isDirty() {
140
    return mDirty.get();
141
  }
142
143
  /**
144
   * Evaluates all R statements in the source document and inserts the
145
   * calculated value into the generated document.
146
   *
147
   * @param text The document text that includes variables that should be
148
   *             replaced with values when rendered as HTML.
149
   * @return The generated document with output from all R statements
150
   * substituted with value returned from their execution.
151
   */
152
  @Override
153
  public String apply( final String text ) {
154
    final int length = text.length();
155
156
    // The * 2 is a wild guess at the ratio of R statements to the length
157
    // of text produced by those statements.
158
    final StringBuilder sb = new StringBuilder( length * 2 );
159
160
    int prevIndex = 0;
161
    int currIndex = text.indexOf( PREFIX );
162
163
    while( currIndex >= 0 ) {
164
      // Copy everything up to, but not including, an R statement (`r#).
165
      sb.append( text, prevIndex, currIndex );
166
167
      // Jump to the start of the R statement.
168
      prevIndex = currIndex + PREFIX_LENGTH;
169
170
      // Find the statement ending (`), without indexing past the text boundary.
171
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
172
173
      // Only evaluate inline R statements that have end delimiters.
174
      if( currIndex > 1 ) {
175
        // Extract the inline R statement to be evaluated.
176
        final String r = text.substring( prevIndex, currIndex );
177
178
        // Pass the R statement into the R engine for evaluation.
179
        try {
180
          final Object result = evalText( r );
181
182
          // Append the string representation of the result into the text.
183
          sb.append( result );
184
        } catch( final Exception e ) {
185
          // If the string couldn't be parsed using R, append the statement
186
          // that failed to parse, instead of its evaluated value.
187
          sb.append( PREFIX ).append( r ).append( SUFFIX );
188
189
          // Tell the user that there was a problem.
190
          alert( STATUS_PARSE_ERROR, e.getMessage(), currIndex );
191
        }
192
193
        // Retain the R statement's ending position in the text.
194
        prevIndex = currIndex + 1;
195
      }
196
197
      // Find the start of the next inline R statement.
198
      currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
199
    }
200
201
    // Copy from the previous index to the end of the string.
202
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
203
  }
204
205
  /**
206
   * Look up an R expression from the cache then return the resulting object.
207
   * If the R expression hasn't been cached, it'll first be evaluated.
208
   *
209
   * @param r The expression to evaluate.
210
   * @return The object resulting from the evaluation.
211
   */
212
  private Object evalText( final String r ) {
213
    return mEvalCache.computeIfAbsent( r, v -> eval( r ) );
214
  }
215
216
  /**
217
   * Evaluate an R expression and return the resulting object.
218
   *
219
   * @param r The expression to evaluate.
220
   * @return The object resulting from the evaluation.
221
   */
222
  private Object eval( final String r ) {
223
    try {
224
      return getScriptEngine().eval( r );
225
    } catch( final Exception ex ) {
226
      final String expr = r.substring( 0, min( r.length(), 30 ) );
227
      alert( "Main.status.error.r", expr, ex.getMessage() );
228
    }
229
230
    return "";
231
  }
232
233
  /**
234
   * Return the given path if not {@code null}, otherwise return the path to
235
   * the user's directory.
236
   *
237
   * @return A non-null path.
238
   */
239
  private Path getWorkingDirectory() {
240
    return getUserPreferences().getRDirectory().toPath();
241
  }
242
243
  private ObjectProperty<File> workingDirectoryProperty() {
244
    return getUserPreferences().rDirectoryProperty();
245
  }
246
247
  /**
248
   * Loads the R init script from the application's persisted preferences.
249
   *
250
   * @return A non-null string, possibly empty.
251
   */
252
  private String getBootstrapScript() {
253
    return getUserPreferences().getRScript();
254
  }
255
256
  private StringProperty bootstrapScriptProperty() {
257
    return getUserPreferences().rScriptProperty();
258
  }
259
260
  private UserPreferences getUserPreferences() {
261
    return UserPreferences.getInstance();
262
  }
263
264
  private ScriptEngine getScriptEngine() {
265
    return ENGINE;
266
  }
267
}
2681
D src/main/java/com/scrivenvar/processors/Processor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import java.util.function.UnaryOperator;
31
32
/**
33
 * Responsible for processing documents from one known format to another.
34
 * Processes the given content providing a transformation from one document
35
 * format into another. For example, this could convert from XML to text using
36
 * an XSLT processor, or from markdown to HTML.
37
 *
38
 * @param <T> The type of processor to create.
39
 */
40
public interface Processor<T> extends UnaryOperator<T> {
41
42
  /**
43
   * Removes the given processor from the chain, returning a new immutable
44
   * chain equivalent to this chain, but without the given processor.
45
   *
46
   * @param processor The {@link Processor} to remove from the chain.
47
   * @return A delegating processor chain starting from this processor
48
   * onwards with the given processor removed from the chain.
49
   */
50
  Processor<T> remove( Class<? extends Processor<T>> processor );
51
52
  /**
53
   * Adds a document processor to call after this processor finishes processing
54
   * the document given to the process method.
55
   *
56
   * @return The processor that should transform the document after this
57
   * instance has finished processing, or {@code null} if this is the last
58
   * processor in the chain.
59
   */
60
  default Processor<T> next() {
61
    return null;
62
  }
63
}
641
D src/main/java/com/scrivenvar/processors/ProcessorFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.FileEditorTab;
32
import com.scrivenvar.preview.HTMLPreviewPane;
33
import com.scrivenvar.processors.markdown.MarkdownProcessor;
34
35
import java.util.Map;
36
37
/**
38
 * Responsible for creating processors capable of parsing, transforming,
39
 * interpolating, and rendering known file types.
40
 */
41
public class ProcessorFactory extends AbstractFileFactory {
42
43
  private final HTMLPreviewPane mPreviewPane;
44
  private final Map<String, String> mResolvedMap;
45
  private final Processor<String> mMarkdownProcessor;
46
47
  /**
48
   * Constructs a factory with the ability to create processors that can perform
49
   * text and caret processing to generate a final preview.
50
   *
51
   * @param previewPane Where the final output is rendered.
52
   * @param resolvedMap Flat map of definitions to replace before final render.
53
   */
54
  public ProcessorFactory(
55
      final HTMLPreviewPane previewPane,
56
      final Map<String, String> resolvedMap ) {
57
    mPreviewPane = previewPane;
58
    mResolvedMap = resolvedMap;
59
    mMarkdownProcessor = createMarkdownProcessor();
60
  }
61
62
  /**
63
   * Creates a processor chain suitable for parsing and rendering the file
64
   * opened at the given tab.
65
   *
66
   * @param tab The tab containing a text editor, path, and caret position.
67
   * @return A processor that can render the given tab's text.
68
   */
69
  public Processor<String> createProcessors( final FileEditorTab tab ) {
70
    return switch( lookup( tab.getPath() ) ) {
71
      case RMARKDOWN -> createRProcessor();
72
      case SOURCE -> createMarkdownDefinitionProcessor();
73
      case XML -> createXMLProcessor( tab );
74
      case RXML -> createRXMLProcessor( tab );
75
      default -> createIdentityProcessor();
76
    };
77
  }
78
79
  private Processor<String> createHTMLPreviewProcessor() {
80
    return new HtmlPreviewProcessor( getPreviewPane() );
81
  }
82
83
  /**
84
   * Creates and links the processors at the end of the processing chain.
85
   *
86
   * @return A markdown, caret replacement, and preview pane processor chain.
87
   */
88
  private Processor<String> createMarkdownProcessor() {
89
    final var hpp = createHTMLPreviewProcessor();
90
    return new MarkdownProcessor( hpp, getPreviewPane().getPath() );
91
  }
92
93
  protected Processor<String> createIdentityProcessor() {
94
    final var hpp = createHTMLPreviewProcessor();
95
    return new IdentityProcessor( hpp );
96
  }
97
98
  protected Processor<String> createDefinitionProcessor(
99
      final Processor<String> p ) {
100
    return new DefinitionProcessor( p, getResolvedMap() );
101
  }
102
103
  protected Processor<String> createMarkdownDefinitionProcessor() {
104
    final var tpc = getCommonProcessor();
105
    return createDefinitionProcessor( tpc );
106
  }
107
108
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
109
    final var tpc = getCommonProcessor();
110
    final var xmlp = new XmlProcessor( tpc, tab.getPath() );
111
    return createDefinitionProcessor( xmlp );
112
  }
113
114
  protected Processor<String> createRProcessor() {
115
    final var tpc = getCommonProcessor();
116
    final var rp = new InlineRProcessor( tpc, getResolvedMap() );
117
    return new RVariableProcessor( rp, getResolvedMap() );
118
  }
119
120
  protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
121
    final var tpc = getCommonProcessor();
122
    final var xmlp = new XmlProcessor( tpc, tab.getPath() );
123
    final var rp = new InlineRProcessor( xmlp, getResolvedMap() );
124
    return new RVariableProcessor( rp, getResolvedMap() );
125
  }
126
127
  private HTMLPreviewPane getPreviewPane() {
128
    return mPreviewPane;
129
  }
130
131
  /**
132
   * Returns the variable map of interpolated definitions.
133
   *
134
   * @return A map to help dereference variables.
135
   */
136
  private Map<String, String> getResolvedMap() {
137
    return mResolvedMap;
138
  }
139
140
  /**
141
   * Returns a processor common to all processors: markdown, caret position
142
   * token replacer, and an HTML preview renderer.
143
   *
144
   * @return Processors at the end of the processing chain.
145
   */
146
  private Processor<String> getCommonProcessor() {
147
    return mMarkdownProcessor;
148
  }
149
}
1501
D src/main/java/com/scrivenvar/processors/RVariableProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.sigils.RSigilOperator;
31
32
import java.util.HashMap;
33
import java.util.Map;
34
35
/**
36
 * Converts the keys of the resolved map from default form to R form, then
37
 * performs a substitution on the text. The default R variable syntax is
38
 * {@code v$tree$leaf}.
39
 */
40
public class RVariableProcessor extends DefinitionProcessor {
41
42
  public RVariableProcessor(
43
      final Processor<String> rp, final Map<String, String> map ) {
44
    super( rp, map );
45
  }
46
47
  /**
48
   * Returns the R-based version of the interpolated variable definitions.
49
   *
50
   * @return Variable names transmogrified from the default syntax to R syntax.
51
   */
52
  @Override
53
  protected Map<String, String> getDefinitions() {
54
    return toR( super.getDefinitions() );
55
  }
56
57
  /**
58
   * Converts the given map from regular variables to R variables.
59
   *
60
   * @param map Map of variable names to values.
61
   * @return Map of R variables.
62
   */
63
  private Map<String, String> toR( final Map<String, String> map ) {
64
    final var rMap = new HashMap<String, String>( map.size() );
65
66
    for( final var entry : map.entrySet() ) {
67
      final var key = entry.getKey();
68
      rMap.put( RSigilOperator.entoken( key ), toRValue( map.get( key ) ) );
69
    }
70
71
    return rMap;
72
  }
73
74
  private String toRValue( final String value ) {
75
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
76
  }
77
78
  /**
79
   * TODO: Make generic method for replacing text.
80
   *
81
   * @param haystack Search this string for the needle, must not be null.
82
   * @param needle   The character to find in the haystack.
83
   * @param thread   Replace the needle with this text, if the needle is found.
84
   * @return The haystack with the all instances of needle replaced with thread.
85
   */
86
  @SuppressWarnings("SameParameterValue")
87
  private String escape(
88
      final String haystack, final char needle, final String thread ) {
89
    int end = haystack.indexOf( needle );
90
91
    if( end < 0 ) {
92
      return haystack;
93
    }
94
95
    final int length = haystack.length();
96
    int start = 0;
97
98
    // Replace up to 32 occurrences before the string reallocates its buffer.
99
    final StringBuilder sb = new StringBuilder( length + 32 );
100
101
    while( end >= 0 ) {
102
      sb.append( haystack, start, end ).append( thread );
103
      start = end + 1;
104
      end = haystack.indexOf( needle, start );
105
    }
106
107
    return sb.append( haystack.substring( start ) ).toString();
108
  }
109
}
1101
D src/main/java/com/scrivenvar/processors/XmlProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.Snitch;
32
import net.sf.saxon.TransformerFactoryImpl;
33
import net.sf.saxon.trans.XPathException;
34
35
import javax.xml.stream.XMLEventReader;
36
import javax.xml.stream.XMLInputFactory;
37
import javax.xml.stream.XMLStreamException;
38
import javax.xml.stream.events.ProcessingInstruction;
39
import javax.xml.stream.events.XMLEvent;
40
import javax.xml.transform.*;
41
import javax.xml.transform.stream.StreamResult;
42
import javax.xml.transform.stream.StreamSource;
43
import java.io.File;
44
import java.io.Reader;
45
import java.io.StringReader;
46
import java.io.StringWriter;
47
import java.nio.file.Path;
48
import java.nio.file.Paths;
49
50
import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
51
52
/**
53
 * Transforms an XML document. The XML document must have a stylesheet specified
54
 * as part of its processing instructions, such as:
55
 * <p>
56
 * {@code xml-stylesheet type="text/xsl" href="markdown.xsl"}
57
 * </p>
58
 * <p>
59
 * The XSL must transform the XML document into Markdown, or another format
60
 * recognized by the next link on the chain.
61
 * </p>
62
 */
63
public class XmlProcessor extends AbstractProcessor<String>
64
    implements ErrorListener {
65
66
  private final Snitch snitch = Services.load( Snitch.class );
67
68
  private XMLInputFactory xmlInputFactory;
69
  private TransformerFactory transformerFactory;
70
  private Transformer transformer;
71
72
  private Path path;
73
74
  /**
75
   * Constructs an XML processor that can transform an XML document into another
76
   * format based on the XSL file specified as a processing instruction. The
77
   * path must point to the directory where the XSL file is found, which implies
78
   * that they must be in the same directory.
79
   *
80
   * @param processor Next link in the processing chain.
81
   * @param path      The path to the XML file content to be processed.
82
   */
83
  public XmlProcessor( final Processor<String> processor, final Path path ) {
84
    super( processor );
85
    setPath( path );
86
  }
87
88
  /**
89
   * Transforms the given XML text into another form (typically Markdown).
90
   *
91
   * @param text The text to transform, can be empty, cannot be null.
92
   * @return The transformed text, or empty if text is empty.
93
   */
94
  @Override
95
  public String apply( final String text ) {
96
    try {
97
      return text.isEmpty() ? text : transform( text );
98
    } catch( final Exception ex ) {
99
      throw new RuntimeException( ex );
100
    }
101
  }
102
103
  /**
104
   * Performs an XSL transformation on the given XML text. The XML text must
105
   * have a processing instruction that points to the XSL template file to use
106
   * for the transformation.
107
   *
108
   * @param text The text to transform.
109
   * @return The transformed text.
110
   */
111
  private String transform( final String text ) throws Exception {
112
    // Extract the XML stylesheet processing instruction.
113
    final String template = getXsltFilename( text );
114
    final Path xsl = getXslPath( template );
115
116
    try(
117
        final StringWriter output = new StringWriter( text.length() );
118
        final StringReader input = new StringReader( text ) ) {
119
120
      // Listen for external file modification events.
121
      getSnitch().listen( xsl );
122
123
      getTransformer( xsl ).transform(
124
          new StreamSource( input ),
125
          new StreamResult( output )
126
      );
127
128
      return output.toString();
129
    }
130
  }
131
132
  /**
133
   * Returns an XSL transformer ready to transform an XML document using the
134
   * XSLT file specified by the given path. If the path is already known then
135
   * this will return the associated transformer.
136
   *
137
   * @param xsl The path to an XSLT file.
138
   * @return A transformer that will transform XML documents using the given
139
   * XSLT file.
140
   * @throws TransformerConfigurationException Could not instantiate the
141
   *                                           transformer.
142
   */
143
  private Transformer getTransformer( final Path xsl )
144
      throws TransformerConfigurationException {
145
    if( this.transformer == null ) {
146
      this.transformer = createTransformer( xsl );
147
    }
148
149
    return this.transformer;
150
  }
151
152
  /**
153
   * Creates a configured transformer ready to run.
154
   *
155
   * @param xsl The stylesheet to use for transforming XML documents.
156
   * @return The edited XML document transformed into another format (usually
157
   * markdown).
158
   * @throws TransformerConfigurationException Could not create the transformer.
159
   */
160
  protected Transformer createTransformer( final Path xsl )
161
      throws TransformerConfigurationException {
162
    final Source xslt = new StreamSource( xsl.toFile() );
163
164
    return getTransformerFactory().newTransformer( xslt );
165
  }
166
167
  private Path getXslPath( final String filename ) {
168
    final Path xmlPath = getPath();
169
    final File xmlDirectory = xmlPath.toFile().getParentFile();
170
171
    return Paths.get( xmlDirectory.getPath(), filename );
172
  }
173
174
  /**
175
   * Given XML text, this will use a StAX pull reader to obtain the XML
176
   * stylesheet processing instruction. This will throw a parse exception if the
177
   * href pseudo-attribute filename value cannot be found.
178
   *
179
   * @param xml The XML containing an xml-stylesheet processing instruction.
180
   * @return The href pseudo-attribute value.
181
   * @throws XMLStreamException Could not parse the XML file.
182
   */
183
  private String getXsltFilename( final String xml )
184
      throws XMLStreamException, XPathException {
185
186
    String result = "";
187
188
    try( final StringReader sr = new StringReader( xml ) ) {
189
      boolean found = false;
190
      int count = 0;
191
      final XMLEventReader reader = createXMLEventReader( sr );
192
193
      // If the processing instruction wasn't found in the first 10 lines,
194
      // fail fast. This should iterate twice through the loop.
195
      while( !found && reader.hasNext() && count++ < 10 ) {
196
        final XMLEvent event = reader.nextEvent();
197
198
        if( event.isProcessingInstruction() ) {
199
          final ProcessingInstruction pi = (ProcessingInstruction) event;
200
          final String target = pi.getTarget();
201
202
          if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
203
            result = getPseudoAttribute( pi.getData(), "href" );
204
            found = true;
205
          }
206
        }
207
      }
208
    }
209
210
    return result;
211
  }
212
213
  private XMLEventReader createXMLEventReader( final Reader reader )
214
      throws XMLStreamException {
215
    return getXMLInputFactory().createXMLEventReader( reader );
216
  }
217
218
  private synchronized XMLInputFactory getXMLInputFactory() {
219
    if( this.xmlInputFactory == null ) {
220
      this.xmlInputFactory = createXMLInputFactory();
221
    }
222
223
    return this.xmlInputFactory;
224
  }
225
226
  private XMLInputFactory createXMLInputFactory() {
227
    return XMLInputFactory.newInstance();
228
  }
229
230
  private synchronized TransformerFactory getTransformerFactory() {
231
    if( this.transformerFactory == null ) {
232
      this.transformerFactory = createTransformerFactory();
233
    }
234
235
    return this.transformerFactory;
236
  }
237
238
  /**
239
   * Returns a high-performance XSLT 2 transformation engine.
240
   *
241
   * @return An XSL transforming engine.
242
   */
243
  private TransformerFactory createTransformerFactory() {
244
    final TransformerFactory factory = new TransformerFactoryImpl();
245
246
    // Bubble problems up to the user interface, rather than standard error.
247
    factory.setErrorListener( this );
248
249
    return factory;
250
  }
251
252
  /**
253
   * Called when the XSL transformer issues a warning.
254
   *
255
   * @param ex The problem the transformer encountered.
256
   */
257
  @Override
258
  public void warning( final TransformerException ex ) {
259
    throw new RuntimeException( ex );
260
  }
261
262
  /**
263
   * Called when the XSL transformer issues an error.
264
   *
265
   * @param ex The problem the transformer encountered.
266
   */
267
  @Override
268
  public void error( final TransformerException ex ) {
269
    throw new RuntimeException( ex );
270
  }
271
272
  /**
273
   * Called when the XSL transformer issues a fatal error, which is probably
274
   * a bit over-dramatic a method name.
275
   *
276
   * @param ex The problem the transformer encountered.
277
   */
278
  @Override
279
  public void fatalError( final TransformerException ex ) {
280
    throw new RuntimeException( ex );
281
  }
282
283
  private void setPath( final Path path ) {
284
    this.path = path;
285
  }
286
287
  private Path getPath() {
288
    return this.path;
289
  }
290
291
  private Snitch getSnitch() {
292
    return this.snitch;
293
  }
294
}
2951
D 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.ast.ListBlock;
5
import com.vladsch.flexmark.html.AttributeProvider;
6
import com.vladsch.flexmark.html.AttributeProviderFactory;
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
import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX;
17
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
18
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
19
import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
20
21
/**
22
 * Responsible for giving most block-level elements a unique identifier
23
 * attribute. The identifier is used to coordinate scrolling.
24
 */
25
public class BlockExtension implements HtmlRendererExtension {
26
  /**
27
   * Responsible for creating the id attribute. This class is instantiated
28
   * each time the document is rendered, thereby resetting the count to zero.
29
   */
30
  public static class IdAttributeProvider implements AttributeProvider {
31
    private int mCount;
32
33
    private static AttributeProviderFactory createFactory() {
34
      return new IndependentAttributeProviderFactory() {
35
        @Override
36
        public @NotNull AttributeProvider apply(
37
            @NotNull final LinkResolverContext context ) {
38
          return new IdAttributeProvider();
39
        }
40
      };
41
    }
42
43
    @Override
44
    public void setAttributes( @NotNull Node node,
45
                               @NotNull AttributablePart part,
46
                               @NotNull MutableAttributes attributes ) {
47
      // Blockquotes are troublesome because they can interleave blank lines
48
      // without having an equivalent blank line in the source document. That
49
      // is, in Markdown the > symbol on a line by itself will generate a blank
50
      // line in the resulting document; however, a > symbol in the text editor
51
      // does not count as a blank line. Resolving this issue is tricky.
52
      //
53
      // The CODE_CONTENT represents <code> embedded inside <pre>; both elements
54
      // enter this method as FencedCodeBlock, but only the <pre> must be
55
      // uniquely identified (because they are the same line in Markdown).
56
      //
57
      if( node instanceof Block &&
58
          !(node instanceof BlockQuote) &&
59
          !(node instanceof ListBlock) &&
60
          (part != CODE_CONTENT) ) {
61
        attributes.addValue( "id", PARAGRAPH_ID_PREFIX + mCount++ );
62
      }
63
    }
64
  }
65
66
  private BlockExtension() {
67
  }
68
69
  @Override
70
  public void extend( final Builder builder,
71
                      @NotNull final String rendererType ) {
72
    builder.attributeProviderFactory( IdAttributeProvider.createFactory() );
73
  }
74
75
  public static BlockExtension create() {
76
    return new BlockExtension();
77
  }
78
79
  @Override
80
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
81
  }
82
}
831
D src/main/java/com/scrivenvar/processors/markdown/ImageLinkExtension.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.markdown;
29
30
import com.scrivenvar.preferences.UserPreferences;
31
import com.vladsch.flexmark.ast.Image;
32
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
33
import com.vladsch.flexmark.html.LinkResolver;
34
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
35
import com.vladsch.flexmark.html.renderer.LinkStatus;
36
import com.vladsch.flexmark.html.renderer.ResolvedLink;
37
import com.vladsch.flexmark.util.ast.Node;
38
import com.vladsch.flexmark.util.data.MutableDataHolder;
39
import org.jetbrains.annotations.NotNull;
40
import org.renjin.repackaged.guava.base.Splitter;
41
42
import java.io.File;
43
import java.io.FileNotFoundException;
44
import java.nio.file.Path;
45
46
import static com.scrivenvar.StatusBarNotifier.alert;
47
import static com.scrivenvar.util.ProtocolResolver.getProtocol;
48
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
49
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
50
import static java.lang.String.format;
51
52
/**
53
 * Responsible for ensuring that images can be rendered relative to a path.
54
 * This allows images to be located virtually anywhere.
55
 */
56
public class ImageLinkExtension implements HtmlRendererExtension {
57
58
  /**
59
   * Creates an extension capable of using a relative path to embed images.
60
   *
61
   * @param path The {@link Path} to the file being edited; the parent path
62
   *             is the starting location of the relative image directory.
63
   * @return The new {@link ImageLinkExtension}, never {@code null}.
64
   */
65
  public static ImageLinkExtension create( @NotNull final Path path ) {
66
    return new ImageLinkExtension( path );
67
  }
68
69
  private class Factory extends IndependentLinkResolverFactory {
70
    @Override
71
    public @NotNull LinkResolver apply(
72
        @NotNull final LinkResolverBasicContext context ) {
73
      return new ImageLinkResolver();
74
    }
75
  }
76
77
  private class ImageLinkResolver implements LinkResolver {
78
    private final UserPreferences mUserPref = getUserPreferences();
79
    private final File mImagesUserPrefix = mUserPref.getImagesDirectory();
80
    private final String mImageExtensions = mUserPref.getImagesOrder();
81
82
    public ImageLinkResolver() {
83
    }
84
85
    /**
86
     * You can also set/clear/modify attributes through
87
     * {@link ResolvedLink#getAttributes()} and
88
     * {@link ResolvedLink#getNonNullAttributes()}.
89
     */
90
    @NotNull
91
    @Override
92
    public ResolvedLink resolveLink(
93
        @NotNull final Node node,
94
        @NotNull final LinkResolverBasicContext context,
95
        @NotNull final ResolvedLink link ) {
96
      return node instanceof Image ? resolve( link ) : link;
97
    }
98
99
    private ResolvedLink resolve( final ResolvedLink link ) {
100
      var url = link.getUrl();
101
      final var protocol = getProtocol( url );
102
103
      try {
104
        // If the direct file name exists, then use it directly.
105
        if( (protocol.isFile() && Path.of( url ).toFile().exists()) ||
106
            protocol.isHttp() ) {
107
          return valid( link, url );
108
        }
109
      } catch( final Exception ignored ) {
110
        // Try to resolve the image, dynamically.
111
      }
112
113
      try {
114
        final Path imagePrefix = getImagePrefix().toPath();
115
116
        // Path to the file being edited.
117
        Path editPath = getEditPath();
118
119
        // If there is no parent path to the file, it means the file has not
120
        // been saved. Default to using the value from the user's preferences.
121
        // The user's preferences will be defaulted to a the application's
122
        // starting directory.
123
        if( editPath == null ) {
124
          editPath = imagePrefix;
125
        }
126
        else {
127
          editPath = Path.of( editPath.toString(), imagePrefix.toString() );
128
        }
129
130
        final Path imagePathPrefix = Path.of( editPath.toString(), url );
131
        final String suffixes = getImageExtensions();
132
        boolean missing = true;
133
134
        // Iterate over the user's preferred image file type extensions.
135
        for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) {
136
          final String imagePath = format( "%s.%s", imagePathPrefix, ext );
137
          final File file = new File( imagePath );
138
139
          if( file.exists() ) {
140
            url = file.toString();
141
            missing = false;
142
            break;
143
          }
144
        }
145
146
        if( missing ) {
147
          throw new FileNotFoundException( imagePathPrefix + ".*" );
148
        }
149
150
        if( protocol.isFile() ) {
151
          url = "file://" + url;
152
        }
153
154
        return valid( link, url );
155
      } catch( final Exception ex ) {
156
        alert( ex );
157
      }
158
159
      return link;
160
    }
161
162
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
163
      return link.withStatus( LinkStatus.VALID ).withUrl( url );
164
    }
165
166
    private File getImagePrefix() {
167
      return mImagesUserPrefix;
168
    }
169
170
    private String getImageExtensions() {
171
      return mImageExtensions;
172
    }
173
174
    private Path getEditPath() {
175
      return mPath.getParent();
176
    }
177
  }
178
179
  private final Path mPath;
180
181
  private ImageLinkExtension( @NotNull final Path path ) {
182
    mPath = path;
183
  }
184
185
  @Override
186
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
187
  }
188
189
  @Override
190
  public void extend( @NotNull final Builder builder,
191
                      @NotNull final String rendererType ) {
192
    builder.linkResolverFactory( new Factory() );
193
  }
194
195
  private UserPreferences getUserPreferences() {
196
    return UserPreferences.getInstance();
197
  }
198
}
1991
D src/main/java/com/scrivenvar/processors/markdown/LigatureExtension.java
1
package com.scrivenvar.processors.markdown;
2
3
import com.vladsch.flexmark.ast.Text;
4
import com.vladsch.flexmark.html.HtmlWriter;
5
import com.vladsch.flexmark.html.renderer.NodeRenderer;
6
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
7
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
8
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
9
import com.vladsch.flexmark.util.ast.TextCollectingVisitor;
10
import com.vladsch.flexmark.util.data.DataHolder;
11
import com.vladsch.flexmark.util.data.MutableDataHolder;
12
import org.jetbrains.annotations.NotNull;
13
import org.jetbrains.annotations.Nullable;
14
15
import java.util.LinkedHashMap;
16
import java.util.Map;
17
import java.util.Set;
18
19
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
20
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
21
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
22
23
/**
24
 * Responsible for substituting multi-codepoint glyphs with single codepoint
25
 * glyphs. The text is adorned with ligatures prior to rendering as HTML.
26
 * This requires a font that supports ligatures.
27
 * <p>
28
 * TODO: I18N https://github.com/DaveJarvis/scrivenvar/issues/81
29
 * </p>
30
 */
31
public class LigatureExtension implements HtmlRendererExtension {
32
  /**
33
   * Retain insertion order so that ligature substitution uses longer ligatures
34
   * ahead of shorter ligatures. The word "ruffian" should use the "ffi"
35
   * ligature, not the "ff" ligature.
36
   */
37
  private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
38
39
  static {
40
    LIGATURES.put( "ffi", "\uFB03" );
41
    LIGATURES.put( "ffl", "\uFB04" );
42
    LIGATURES.put( "ff", "\uFB00" );
43
    LIGATURES.put( "fi", "\uFB01" );
44
    LIGATURES.put( "fl", "\uFB02" );
45
    LIGATURES.put( "ft", "\uFB05" );
46
    LIGATURES.put( "AE", "\u00C6" );
47
    LIGATURES.put( "OE", "\u0152" );
48
//      "ae", "\u00E6",
49
//      "oe", "\u0153",
50
  }
51
52
  private static class LigatureRenderer implements NodeRenderer {
53
    private final TextCollectingVisitor mVisitor = new TextCollectingVisitor();
54
55
    @SuppressWarnings("unused")
56
    public LigatureRenderer( final DataHolder options ) {
57
    }
58
59
    @Override
60
    public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
61
      return Set.of( new NodeRenderingHandler<>(
62
          Text.class, LigatureRenderer.this::render ) );
63
    }
64
65
    /**
66
     * This will pick the fastest string replacement algorithm based on the
67
     * text length. The insertion order of the {@link #LIGATURES} is
68
     * important to give precedence to longer ligatures.
69
     *
70
     * @param textNode The text node containing text to replace with ligatures.
71
     * @param context  Not used.
72
     * @param html     Where to write the text adorned with ligatures.
73
     */
74
    private void render(
75
        @NotNull final Text textNode,
76
        @NotNull final NodeRendererContext context,
77
        @NotNull final HtmlWriter html ) {
78
      final var text = mVisitor.collectAndGetText( textNode );
79
      html.text( replace( text, LIGATURES ) );
80
    }
81
  }
82
83
  private static class Factory implements NodeRendererFactory {
84
    @NotNull
85
    @Override
86
    public NodeRenderer apply( @NotNull DataHolder options ) {
87
      return new LigatureRenderer( options );
88
    }
89
  }
90
91
  private LigatureExtension() {
92
  }
93
94
  @Override
95
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
96
  }
97
98
  @Override
99
  public void extend( @NotNull final Builder builder,
100
                      @NotNull final String rendererType ) {
101
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
102
      builder.nodeRendererFactory( new Factory() );
103
    }
104
  }
105
106
  public static LigatureExtension create() {
107
    return new LigatureExtension();
108
  }
109
}
1101
D src/main/java/com/scrivenvar/processors/markdown/MarkdownProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.markdown;
29
30
import com.scrivenvar.processors.AbstractProcessor;
31
import com.scrivenvar.processors.Processor;
32
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
33
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
34
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
35
import com.vladsch.flexmark.ext.tables.TablesExtension;
36
import com.vladsch.flexmark.ext.typographic.TypographicExtension;
37
import com.vladsch.flexmark.html.HtmlRenderer;
38
import com.vladsch.flexmark.parser.Parser;
39
import com.vladsch.flexmark.util.ast.IParse;
40
import com.vladsch.flexmark.util.ast.Node;
41
import com.vladsch.flexmark.util.misc.Extension;
42
43
import java.nio.file.Path;
44
import java.util.ArrayList;
45
import java.util.Collection;
46
47
import static com.scrivenvar.Constants.USER_DIRECTORY;
48
49
/**
50
 * Responsible for parsing a Markdown document and rendering it as HTML.
51
 */
52
public class MarkdownProcessor extends AbstractProcessor<String> {
53
54
  private final HtmlRenderer mRenderer;
55
  private final IParse mParser;
56
57
  public MarkdownProcessor(
58
      final Processor<String> successor ) {
59
    this( successor, Path.of( USER_DIRECTORY ) );
60
  }
61
62
  /**
63
   * Constructs a new Markdown processor that can create HTML documents.
64
   *
65
   * @param successor Usually the HTML Preview Processor.
66
   */
67
  public MarkdownProcessor(
68
      final Processor<String> successor, final Path path ) {
69
    super( successor );
70
71
    // Standard extensions
72
    final Collection<Extension> extensions = new ArrayList<>();
73
    extensions.add( DefinitionExtension.create() );
74
    extensions.add( StrikethroughSubscriptExtension.create() );
75
    extensions.add( SuperscriptExtension.create() );
76
    extensions.add( TablesExtension.create() );
77
    extensions.add( TypographicExtension.create() );
78
79
    // Allows referencing image files via relative paths and dynamic file types.
80
    extensions.add( ImageLinkExtension.create( path ) );
81
    extensions.add( BlockExtension.create() );
82
    extensions.add( TeXExtension.create() );
83
84
    // TODO: https://github.com/FAlthausen/Vollkorn-Typeface/issues/38
85
    // TODO: Uncomment when Vollkorn ligatures are fixed.
86
    // extensions.add( LigatureExtension.create() );
87
88
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
89
    mParser = Parser.builder()
90
                    .extensions( extensions )
91
                    .build();
92
  }
93
94
  /**
95
   * Converts the given Markdown string into HTML, without the doctype, html,
96
   * head, and body tags.
97
   *
98
   * @param markdown The string to convert from Markdown to HTML.
99
   * @return The HTML representation of the Markdown document.
100
   */
101
  @Override
102
  public String apply( final String markdown ) {
103
    return toHtml( markdown );
104
  }
105
106
  /**
107
   * Returns the AST in the form of a node for the given markdown document. This
108
   * can be used, for example, to determine if a hyperlink exists inside of a
109
   * paragraph.
110
   *
111
   * @param markdown The markdown to convert into an AST.
112
   * @return The markdown AST for the given text (usually a paragraph).
113
   */
114
  public Node toNode( final String markdown ) {
115
    return parse( markdown );
116
  }
117
118
  /**
119
   * Helper method to create an AST given some markdown.
120
   *
121
   * @param markdown The markdown to parse.
122
   * @return The root node of the markdown tree.
123
   */
124
  private Node parse( final String markdown ) {
125
    return getParser().parse( markdown );
126
  }
127
128
  /**
129
   * Converts a string of markdown into HTML.
130
   *
131
   * @param markdown The markdown text to convert to HTML, must not be null.
132
   * @return The markdown rendered as an HTML document.
133
   */
134
  private String toHtml( final String markdown ) {
135
    return getRenderer().render( parse( markdown ) );
136
  }
137
138
  /**
139
   * Creates the Markdown document processor.
140
   *
141
   * @return A Parser that can build an abstract syntax tree.
142
   */
143
  private IParse getParser() {
144
    return mParser;
145
  }
146
147
  private HtmlRenderer getRenderer() {
148
    return mRenderer;
149
  }
150
}
1511
D src/main/java/com/scrivenvar/processors/markdown/TeXExtension.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.markdown;
29
30
import com.scrivenvar.processors.markdown.tex.TeXInlineDelimiterProcessor;
31
import com.scrivenvar.processors.markdown.tex.TeXNodeRenderer;
32
import com.vladsch.flexmark.html.HtmlRenderer;
33
import com.vladsch.flexmark.parser.Parser;
34
import com.vladsch.flexmark.util.data.MutableDataHolder;
35
import org.jetbrains.annotations.NotNull;
36
37
import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
38
import static com.vladsch.flexmark.parser.Parser.ParserExtension;
39
40
/**
41
 * Responsible for wrapping delimited TeX code in Markdown into an XML element
42
 * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
43
 * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
44
 * is responsible for converting the TeX code for display. This avoids inserting
45
 * SVG code into the Markdown document, which the parser would then have to
46
 * iterate---a <em>very</em> wasteful operation that impacts front-end
47
 * performance.
48
 */
49
public class TeXExtension implements ParserExtension, HtmlRendererExtension {
50
  /**
51
   * Creates an extension capable of handling delimited TeX code in Markdown.
52
   *
53
   * @return The new {@link TeXExtension}, never {@code null}.
54
   */
55
  public static TeXExtension create() {
56
    return new TeXExtension();
57
  }
58
59
  /**
60
   * Force using the {@link #create()} method for consistency.
61
   */
62
  private TeXExtension() {
63
  }
64
65
  /**
66
   * Adds the TeX extension for HTML document export types.
67
   *
68
   * @param builder      The document builder.
69
   * @param rendererType Indicates the document type to be built.
70
   */
71
  @Override
72
  public void extend( @NotNull final HtmlRenderer.Builder builder,
73
                      @NotNull final String rendererType ) {
74
    if( "HTML".equalsIgnoreCase( rendererType ) ) {
75
      builder.nodeRendererFactory( new TeXNodeRenderer.Factory() );
76
    }
77
  }
78
79
  @Override
80
  public void extend( final Parser.Builder builder ) {
81
    builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() );
82
  }
83
84
  @Override
85
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
86
  }
87
88
  @Override
89
  public void parserOptions( final MutableDataHolder options ) {
90
  }
91
}
921
D src/main/java/com/scrivenvar/processors/markdown/tex/TeXInlineDelimiterProcessor.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.markdown.tex;
29
30
import com.vladsch.flexmark.parser.InlineParser;
31
import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
32
import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
33
import com.vladsch.flexmark.parser.delimiter.DelimiterRun;
34
import com.vladsch.flexmark.util.ast.Node;
35
36
public class TeXInlineDelimiterProcessor implements DelimiterProcessor {
37
38
  @Override
39
  public void process( final Delimiter opener, final Delimiter closer,
40
                       final int delimitersUsed ) {
41
    final var node = new TeXNode();
42
    opener.moveNodesBetweenDelimitersTo(node, closer);
43
  }
44
45
  @Override
46
  public char getOpeningCharacter() {
47
    return '$';
48
  }
49
50
  @Override
51
  public char getClosingCharacter() {
52
    return '$';
53
  }
54
55
  @Override
56
  public int getMinLength() {
57
    return 1;
58
  }
59
60
  /**
61
   * Allow for $ or $$.
62
   *
63
   * @param opener One or more opening delimiter characters.
64
   * @param closer One or more closing delimiter characters.
65
   * @return The number of delimiters to use to determine whether a valid
66
   * opening delimiter expression is found.
67
   */
68
  @Override
69
  public int getDelimiterUse(
70
      final DelimiterRun opener, final DelimiterRun closer ) {
71
    return 1;
72
  }
73
74
  @Override
75
  public boolean canBeOpener( final String before,
76
                              final String after,
77
                              final boolean leftFlanking,
78
                              final boolean rightFlanking,
79
                              final boolean beforeIsPunctuation,
80
                              final boolean afterIsPunctuation,
81
                              final boolean beforeIsWhitespace,
82
                              final boolean afterIsWhiteSpace ) {
83
    return leftFlanking;
84
  }
85
86
  @Override
87
  public boolean canBeCloser( final String before,
88
                              final String after,
89
                              final boolean leftFlanking,
90
                              final boolean rightFlanking,
91
                              final boolean beforeIsPunctuation,
92
                              final boolean afterIsPunctuation,
93
                              final boolean beforeIsWhitespace,
94
                              final boolean afterIsWhiteSpace ) {
95
    return rightFlanking;
96
  }
97
98
  @Override
99
  public Node unmatchedDelimiterNode(
100
      final InlineParser inlineParser, final DelimiterRun delimiter ) {
101
    return null;
102
  }
103
104
  @Override
105
  public boolean skipNonOpenerCloser() {
106
    return false;
107
  }
108
}
1091
D src/main/java/com/scrivenvar/processors/markdown/tex/TeXNode.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.markdown.tex;
29
30
import com.vladsch.flexmark.ast.DelimitedNodeImpl;
31
32
public class TeXNode extends DelimitedNodeImpl {
33
  /**
34
   * TeX expression wrapped in a {@code <tex>} element.
35
   */
36
  public static final String HTML_TEX = "tex";
37
38
  public TeXNode() {
39
  }
40
}
411
D src/main/java/com/scrivenvar/processors/markdown/tex/TeXNodeRenderer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.markdown.tex;
29
30
import com.vladsch.flexmark.html.HtmlWriter;
31
import com.vladsch.flexmark.html.renderer.NodeRenderer;
32
import com.vladsch.flexmark.html.renderer.NodeRendererContext;
33
import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
34
import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
35
import com.vladsch.flexmark.util.data.DataHolder;
36
import org.jetbrains.annotations.NotNull;
37
import org.jetbrains.annotations.Nullable;
38
39
import java.util.Set;
40
41
import static com.scrivenvar.processors.markdown.tex.TeXNode.HTML_TEX;
42
43
public class TeXNodeRenderer implements NodeRenderer {
44
45
  public static class Factory implements NodeRendererFactory {
46
    @NotNull
47
    @Override
48
    public NodeRenderer apply( @NotNull DataHolder options ) {
49
      return new TeXNodeRenderer();
50
    }
51
  }
52
53
  @Override
54
  public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
55
    return Set.of( new NodeRenderingHandler<>( TeXNode.class, this::render ) );
56
  }
57
58
  private void render( final TeXNode node,
59
                       final NodeRendererContext context,
60
                       final HtmlWriter html ) {
61
    html.tag( HTML_TEX );
62
    html.raw( node.getText() );
63
    html.closeTag( HTML_TEX );
64
  }
65
}
661
D src/main/java/com/scrivenvar/processors/text/AbstractTextReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Responsible for common behaviour across all text replacer implementations.
34
 */
35
public abstract class AbstractTextReplacer implements TextReplacer {
36
37
  /**
38
   * Default (empty) constructor.
39
   */
40
  protected AbstractTextReplacer() {
41
  }
42
43
  protected String[] keys( final Map<String, String> map ) {
44
    return map.keySet().toArray( new String[ 0 ] );
45
  }
46
47
  protected String[] values( final Map<String, String> map ) {
48
    return map.values().toArray( new String[ 0 ] );
49
  }
50
}
511
D src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
import org.ahocorasick.trie.Emit;
32
import org.ahocorasick.trie.Trie.TrieBuilder;
33
import static org.ahocorasick.trie.Trie.builder;
34
35
/**
36
 * Replaces text using an Aho-Corasick algorithm.
37
 */
38
public class AhoCorasickReplacer extends AbstractTextReplacer {
39
40
  /**
41
   * Default (empty) constructor.
42
   */
43
  protected AhoCorasickReplacer() {
44
  }
45
46
  @Override
47
  public String replace( final String text, final Map<String, String> map ) {
48
    // Create a buffer sufficiently large that re-allocations are minimized.
49
    final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
50
51
    // The TrieBuilder should only match whole words and ignore overlaps (there
52
    // shouldn't be any).
53
    final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps();
54
55
    for( final String key : keys( map ) ) {
56
      builder.addKeyword( key );
57
    }
58
59
    int index = 0;
60
61
    // Replace all instances with dereferenced variables.
62
    for( final Emit emit : builder.build().parseText( text ) ) {
63
      sb.append( text, index, emit.getStart() );
64
      sb.append( map.get( emit.getKeyword() ) );
65
      index = emit.getEnd() + 1;
66
    }
67
68
    // Add the remainder of the string (contains no more matches).
69
    sb.append( text.substring( index ) );
70
71
    return sb.toString();
72
  }
73
}
741
D src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
import static org.apache.commons.lang3.StringUtils.replaceEach;
33
34
/**
35
 * Replaces text using Apache's StringUtils.replaceEach method.
36
 */
37
public class StringUtilsReplacer extends AbstractTextReplacer {
38
39
  /**
40
   * Default (empty) constructor.
41
   */
42
  protected StringUtilsReplacer() {
43
  }
44
45
  @Override
46
  public String replace( final String text, final Map<String, String> map ) {
47
    return replaceEach( text, keys( map ), values( map ) );
48
  }
49
}
501
D src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Used to generate a class capable of efficiently replacing variable
34
 * definitions with their values.
35
 */
36
public final class TextReplacementFactory {
37
38
  private static final TextReplacer APACHE = new StringUtilsReplacer();
39
  private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
40
41
  /**
42
   * Returns a text search/replacement instance that is reasonably optimal for
43
   * the given length of text.
44
   *
45
   * @param length The length of text that requires some search and replacing.
46
   * @return A class that can search and replace text with utmost expediency.
47
   */
48
  public static TextReplacer getTextReplacer( final int length ) {
49
    // After about 1,500 characters, the StringUtils implementation is less
50
    // performant than the Aho-Corsick implementation.
51
    //
52
    // See http://stackoverflow.com/a/40836618/59087
53
    return length < 1500 ? APACHE : AHO_CORASICK;
54
  }
55
56
  /**
57
   * Convenience method to instantiate a suitable text replacer algorithm and
58
   * perform a replacement using the given map. At this point, the values should
59
   * be already dereferenced and ready to be substituted verbatim; any
60
   * recursively defined values must have been interpolated previously.
61
   *
62
   * @param text The text containing zero or more variables to replace.
63
   * @param map  The map of variables to their dereferenced values.
64
   * @return The text with all variables replaced.
65
   */
66
  public static String replace(
67
      final String text, final Map<String, String> map ) {
68
    return getTextReplacer( text.length() ).replace( text, map );
69
  }
70
}
711
D src/main/java/com/scrivenvar/processors/text/TextReplacer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Defines the ability to replace text given a set of keys and values.
34
 */
35
public interface TextReplacer {
36
37
  /**
38
   * Searches through the given text for any of the keys given in the map and
39
   * replaces the keys that appear in the text with the key's corresponding
40
   * value.
41
   *
42
   * @param text The text that contains zero or more keys.
43
   * @param map  The set of keys mapped to replacement values.
44
   * @return The given text with all keys replaced with corresponding values.
45
   */
46
  String replace( String text, Map<String, String> map );
47
}
481
D src/main/java/com/scrivenvar/service/Options.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import com.dlsc.preferencesfx.PreferencesFx;
31
32
import java.util.prefs.BackingStoreException;
33
import java.util.prefs.Preferences;
34
35
/**
36
 * Responsible for persisting options that are safe to load before the UI
37
 * is shown. This can include items like window dimensions, last file
38
 * opened, split pane locations, and more. This cannot be used to persist
39
 * options that are user-controlled (i.e., all options available through
40
 * {@link PreferencesFx}).
41
 */
42
public interface Options extends Service {
43
44
  /**
45
   * Returns the {@link Preferences} that persist settings that cannot
46
   * be configured via the user interface.
47
   *
48
   * @return A valid {@link Preferences} instance, never {@code null}.
49
   */
50
  Preferences getState();
51
52
  /**
53
   * Stores the key and value into the user preferences to be loaded the next
54
   * time the application is launched.
55
   *
56
   * @param key   Name of the key to persist along with its value.
57
   * @param value Value to associate with the key.
58
   * @throws BackingStoreException Could not persist the change.
59
   */
60
  void put( String key, String value ) throws BackingStoreException;
61
62
  /**
63
   * Retrieves the value for a key in the user preferences.
64
   *
65
   * @param key          Retrieve the value of this key.
66
   * @param defaultValue The value to return in the event that the given key has
67
   *                     no associated value.
68
   * @return The value associated with the key.
69
   */
70
  String get( String key, String defaultValue );
71
72
  /**
73
   * Retrieves the value for a key in the user preferences. This will return
74
   * the empty string if the value cannot be found.
75
   *
76
   * @param key The key to find in the preferences.
77
   * @return A non-null, possibly empty value for the key.
78
   */
79
  String get( String key );
80
}
811
D src/main/java/com/scrivenvar/service/Service.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
/**
31
 * All services inherit from this one.
32
 */
33
public interface Service {
34
}
351
D src/main/java/com/scrivenvar/service/Settings.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.util.Iterator;
31
import java.util.List;
32
33
/**
34
 * Defines how settings and options can be retrieved.
35
 */
36
public interface Settings extends Service {
37
38
  /**
39
   * Returns a setting property or its default value.
40
   *
41
   * @param property     The property key name to obtain its value.
42
   * @param defaultValue The default value to return iff the property cannot be
43
   *                     found.
44
   * @return The property value for the given property key.
45
   */
46
  String getSetting( String property, String defaultValue );
47
48
  /**
49
   * Returns a setting property or its default value.
50
   *
51
   * @param property     The property key name to obtain its value.
52
   * @param defaultValue The default value to return iff the property cannot be
53
   *                     found.
54
   * @return The property value for the given property key.
55
   */
56
  int getSetting( String property, int defaultValue );
57
58
  /**
59
   * Returns a list of property names that begin with the given prefix. The
60
   * prefix is included in any matching results. This will return keys that
61
   * either match the prefix or start with the prefix followed by a dot ('.').
62
   * For example, a prefix value of <code>the.property.name</code> will likely
63
   * return the expected results, but <code>the.property.name.</code> (note the
64
   * extraneous period) will probably not.
65
   *
66
   * @param prefix The prefix to compare against each property name.
67
   * @return The list of property names that have the given prefix.
68
   */
69
  Iterator<String> getKeys( final String prefix );
70
71
  /**
72
   * Convert the generic list of property objects into strings.
73
   *
74
   * @param property The property value to coerce.
75
   * @param defaults The defaults values to use should the property be unset.
76
   * @return The list of properties coerced from objects to strings.
77
   */
78
  List<String> getStringSettingList( String property, List<String> defaults );
79
80
  /**
81
   * Converts the generic list of property objects into strings.
82
   *
83
   * @param property The property value to coerce.
84
   * @return The list of properties coerced from objects to strings.
85
   */
86
  List<String> getStringSettingList( String property );
87
}
881
D src/main/java/com/scrivenvar/service/Snitch.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.io.IOException;
31
import java.nio.file.Path;
32
import java.util.Observer;
33
34
/**
35
 * Listens for changes to file system files and directories.
36
 */
37
public interface Snitch extends Service, Runnable {
38
39
  /**
40
   * Adds an observer to the set of observers for this object, provided that it
41
   * is not the same as some observer already in the set. The order in which
42
   * notifications will be delivered to multiple observers is not specified.
43
   *
44
   * @param o The object to receive changed events for when monitored files
45
   *          are changed.
46
   */
47
  void addObserver( Observer o );
48
49
  /**
50
   * Listens for changes to the path. If the path specifies a file, then only
51
   * notifications pertaining to that file are sent. Otherwise, change events
52
   * for the directory that contains the file are sent. This method must allow
53
   * for multiple calls to the same file without incurring additional listeners
54
   * or events.
55
   *
56
   * @param file Send notifications when this file changes, can be null.
57
   * @throws IOException Couldn't create a watcher for the given file.
58
   */
59
  void listen( Path file ) throws IOException;
60
61
  /**
62
   * Removes the given file from the notifications list.
63
   *
64
   * @param file The file to stop monitoring for any changes, can be null.
65
   */
66
  void ignore( Path file );
67
68
  /**
69
   * Stop listening for events.
70
   */
71
  void stop();
72
}
731
D src/main/java/com/scrivenvar/service/events/Notification.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
/**
31
 * Represents a message that contains a title and content.
32
 */
33
public interface Notification {
34
35
  /**
36
   * Alert title.
37
   *
38
   * @return A non-null string to use as alert message title.
39
   */
40
  String getTitle();
41
42
  /**
43
   * Alert message content.
44
   *
45
   * @return A non-null string that contains information for the user.
46
   */
47
  String getContent();
48
}
491
D src/main/java/com/scrivenvar/service/events/Notifier.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
import javafx.scene.control.Alert;
31
import javafx.scene.control.ButtonType;
32
import javafx.stage.Window;
33
34
/**
35
 * Provides the application with a uniform way to notify the user of events.
36
 */
37
public interface Notifier {
38
39
  ButtonType YES = ButtonType.YES;
40
  ButtonType NO = ButtonType.NO;
41
  ButtonType CANCEL = ButtonType.CANCEL;
42
43
  /**
44
   * Constructs a default alert message text for a modal alert dialog.
45
   *
46
   * @param title   The dialog box message title.
47
   * @param message The dialog box message content (needs formatting).
48
   * @param args    The arguments to the message content that must be formatted.
49
   * @return The message suitable for building a modal alert dialog.
50
   */
51
  Notification createNotification(
52
      String title,
53
      String message,
54
      Object... args );
55
56
  /**
57
   * Creates an alert of alert type error with a message showing the cause of
58
   * the error.
59
   *
60
   * @param parent  Dialog box owner (for modal purposes).
61
   * @param message The error message, title, and possibly more details.
62
   * @return A modal alert dialog box ready to display using showAndWait.
63
   */
64
  Alert createError( Window parent, Notification message );
65
66
  /**
67
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
68
   *
69
   * @param parent  Dialog box owner (for modal purposes).
70
   * @param message The message, title, and possibly more details.
71
   * @return A modal alert dialog box ready to display using showAndWait.
72
   */
73
  Alert createConfirmation( Window parent, Notification message );
74
}
751
D src/main/java/com/scrivenvar/service/events/impl/ButtonOrderPane.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import javafx.scene.Node;
31
import javafx.scene.control.ButtonBar;
32
import javafx.scene.control.DialogPane;
33
34
import static com.scrivenvar.Constants.SETTINGS;
35
import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
36
37
/**
38
 * Ensures a consistent button order for alert dialogs across platforms (because
39
 * the default button order on Linux defies all logic).
40
 */
41
public class ButtonOrderPane extends DialogPane {
42
43
  @Override
44
  protected Node createButtonBar() {
45
    final var node = (ButtonBar) super.createButtonBar();
46
    node.setButtonOrder( getButtonOrder() );
47
    return node;
48
  }
49
50
  private String getButtonOrder() {
51
    return getSetting( "dialog.alert.button.order.windows",
52
                       BUTTON_ORDER_WINDOWS );
53
  }
54
55
  @SuppressWarnings("SameParameterValue")
56
  private String getSetting( final String key, final String defaultValue ) {
57
    return SETTINGS.getSetting( key, defaultValue );
58
  }
59
}
601
D src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.Notification;
31
32
import java.text.MessageFormat;
33
34
/**
35
 * Responsible for alerting the user to prominent information.
36
 */
37
public class DefaultNotification implements Notification {
38
39
  private final String title;
40
  private final String content;
41
42
  /**
43
   * Constructs default message text for a notification.
44
   *
45
   * @param title   The message title.
46
   * @param message The message content (needs formatting).
47
   * @param args    The arguments to the message content that must be formatted.
48
   */
49
  public DefaultNotification(
50
      final String title,
51
      final String message,
52
      final Object... args ) {
53
    this.title = title;
54
    this.content = MessageFormat.format( message, args );
55
  }
56
57
  @Override
58
  public String getTitle() {
59
    return this.title;
60
  }
61
62
  @Override
63
  public String getContent() {
64
    return this.content;
65
  }
66
}
671
D src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.Notification;
31
import com.scrivenvar.service.events.Notifier;
32
import javafx.scene.control.Alert;
33
import javafx.scene.control.Alert.AlertType;
34
import javafx.stage.Window;
35
36
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
37
import static javafx.scene.control.Alert.AlertType.ERROR;
38
39
/**
40
 * Provides the ability to notify the user of events that need attention,
41
 * such as prompting the user to confirm closing when there are unsaved changes.
42
 */
43
public final class DefaultNotifier implements Notifier {
44
45
  /**
46
   * Contains all the information that the user needs to know about a problem.
47
   *
48
   * @param title   The context for the message.
49
   * @param message The message content (formatted with the given args).
50
   * @param args    Parameters for the message content.
51
   * @return A notification instance, never null.
52
   */
53
  @Override
54
  public Notification createNotification(
55
      final String title,
56
      final String message,
57
      final Object... args ) {
58
    return new DefaultNotification( title, message, args );
59
  }
60
61
  private Alert createAlertDialog(
62
      final Window parent,
63
      final AlertType alertType,
64
      final Notification message ) {
65
66
    final Alert alert = new Alert( alertType );
67
68
    alert.setDialogPane( new ButtonOrderPane() );
69
    alert.setTitle( message.getTitle() );
70
    alert.setHeaderText( null );
71
    alert.setContentText( message.getContent() );
72
    alert.initOwner( parent );
73
74
    return alert;
75
  }
76
77
  @Override
78
  public Alert createConfirmation( final Window parent,
79
                                   final Notification message ) {
80
    final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
81
82
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
83
84
    return alert;
85
  }
86
87
  @Override
88
  public Alert createError( final Window parent, final Notification message ) {
89
    return createAlertDialog( parent, ERROR, message );
90
  }
91
}
921
D src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.service.impl;
28
29
import com.scrivenvar.service.Options;
30
31
import java.util.prefs.BackingStoreException;
32
import java.util.prefs.Preferences;
33
34
import static com.scrivenvar.Constants.PREFS_ROOT;
35
import static com.scrivenvar.Constants.PREFS_STATE;
36
import static java.util.prefs.Preferences.userRoot;
37
38
/**
39
 * Persistent options user can change at runtime.
40
 */
41
public class DefaultOptions implements Options {
42
  public DefaultOptions() {
43
  }
44
45
  /**
46
   * This will throw IllegalArgumentException if the value exceeds the maximum
47
   * preferences value length.
48
   *
49
   * @param key   The name of the key to associate with the value.
50
   * @param value The value to persist.
51
   * @throws BackingStoreException New value not persisted.
52
   */
53
  @Override
54
  public void put( final String key, final String value )
55
      throws BackingStoreException {
56
    getState().put( key, value );
57
    getState().flush();
58
  }
59
60
  @Override
61
  public String get( final String key, final String value ) {
62
    return getState().get( key, value );
63
  }
64
65
  @Override
66
  public String get( final String key ) {
67
    return get( key, "" );
68
  }
69
70
  private Preferences getRootPreferences() {
71
    return userRoot().node( PREFS_ROOT );
72
  }
73
74
  @Override
75
  public Preferences getState() {
76
    return getRootPreferences().node( PREFS_STATE );
77
  }
78
}
791
D src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.impl;
29
30
import com.scrivenvar.service.Settings;
31
import org.apache.commons.configuration2.PropertiesConfiguration;
32
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
33
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
34
import org.apache.commons.configuration2.ex.ConfigurationException;
35
36
import java.io.IOException;
37
import java.io.InputStreamReader;
38
import java.io.Reader;
39
import java.net.URL;
40
import java.nio.charset.Charset;
41
import java.util.Iterator;
42
import java.util.List;
43
44
import static com.scrivenvar.Constants.SETTINGS_NAME;
45
46
/**
47
 * Responsible for loading settings that help avoid hard-coded assumptions.
48
 */
49
public class DefaultSettings implements Settings {
50
51
  private static final char VALUE_SEPARATOR = ',';
52
53
  private PropertiesConfiguration properties;
54
55
  public DefaultSettings() throws ConfigurationException {
56
    setProperties( createProperties() );
57
  }
58
59
  /**
60
   * Returns the value of a string property.
61
   *
62
   * @param property     The property key.
63
   * @param defaultValue The value to return if no property key has been set.
64
   * @return The property key value, or defaultValue when no key found.
65
   */
66
  @Override
67
  public String getSetting( final String property, final String defaultValue ) {
68
    return getSettings().getString( property, defaultValue );
69
  }
70
71
  /**
72
   * Returns the value of a string property.
73
   *
74
   * @param property     The property key.
75
   * @param defaultValue The value to return if no property key has been set.
76
   * @return The property key value, or defaultValue when no key found.
77
   */
78
  @Override
79
  public int getSetting( final String property, final int defaultValue ) {
80
    return getSettings().getInt( property, defaultValue );
81
  }
82
83
  /**
84
   * Convert the generic list of property objects into strings.
85
   *
86
   * @param property The property value to coerce.
87
   * @param defaults The defaults values to use should the property be unset.
88
   * @return The list of properties coerced from objects to strings.
89
   */
90
  @Override
91
  public List<String> getStringSettingList(
92
      final String property, final List<String> defaults ) {
93
    return getSettings().getList( String.class, property, defaults );
94
  }
95
96
  /**
97
   * Convert a list of property objects into strings, with no default value.
98
   *
99
   * @param property The property value to coerce.
100
   * @return The list of properties coerced from objects to strings.
101
   */
102
  @Override
103
  public List<String> getStringSettingList( final String property ) {
104
    return getStringSettingList( property, null );
105
  }
106
107
  /**
108
   * Returns a list of property names that begin with the given prefix.
109
   *
110
   * @param prefix The prefix to compare against each property name.
111
   * @return The list of property names that have the given prefix.
112
   */
113
  @Override
114
  public Iterator<String> getKeys( final String prefix ) {
115
    return getSettings().getKeys( prefix );
116
  }
117
118
  private PropertiesConfiguration createProperties()
119
      throws ConfigurationException {
120
121
    final URL url = getPropertySource();
122
    final PropertiesConfiguration configuration = new PropertiesConfiguration();
123
124
    if( url != null ) {
125
      try( final Reader r = new InputStreamReader( url.openStream(),
126
                                                   getDefaultEncoding() ) ) {
127
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
128
        configuration.read( r );
129
130
      } catch( final IOException ex ) {
131
        throw new RuntimeException( new ConfigurationException( ex ) );
132
      }
133
    }
134
135
    return configuration;
136
  }
137
138
  protected Charset getDefaultEncoding() {
139
    return Charset.defaultCharset();
140
  }
141
142
  protected ListDelimiterHandler createListDelimiterHandler() {
143
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
144
  }
145
146
  private URL getPropertySource() {
147
    return DefaultSettings.class.getResource( getSettingsFilename() );
148
  }
149
150
  private String getSettingsFilename() {
151
    return SETTINGS_NAME;
152
  }
153
154
  private void setProperties( final PropertiesConfiguration configuration ) {
155
    this.properties = configuration;
156
  }
157
158
  private PropertiesConfiguration getSettings() {
159
    return this.properties;
160
  }
161
}
1621
D src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.impl;
29
30
import com.scrivenvar.service.Snitch;
31
32
import java.io.IOException;
33
import java.nio.file.*;
34
import java.util.Collections;
35
import java.util.Map;
36
import java.util.Observable;
37
import java.util.Set;
38
import java.util.concurrent.ConcurrentHashMap;
39
40
import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT;
41
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
42
43
/**
44
 * Listens for file changes. Other classes can register paths to be monitored
45
 * and listen for changes to those paths.
46
 */
47
public class DefaultSnitch extends Observable implements Snitch {
48
49
  /**
50
   * Service for listening to directories for modifications.
51
   */
52
  private WatchService watchService;
53
54
  /**
55
   * Directories being monitored for changes.
56
   */
57
  private Map<WatchKey, Path> keys;
58
59
  /**
60
   * Files that will kick off notification events if modified.
61
   */
62
  private Set<Path> eavesdropped;
63
64
  /**
65
   * Set to true when running; set to false to stop listening.
66
   */
67
  private volatile boolean listening;
68
69
  public DefaultSnitch() {
70
  }
71
72
  @Override
73
  public void stop() {
74
    setListening( false );
75
  }
76
77
  /**
78
   * Adds a listener to the list of files to watch for changes. If the file is
79
   * already in the monitored list, this will return immediately.
80
   *
81
   * @param file Path to a file to watch for changes.
82
   * @throws IOException The file could not be monitored.
83
   */
84
  @Override
85
  public void listen( final Path file ) throws IOException {
86
    if( file != null && getEavesdropped().add( file ) ) {
87
      final Path dir = toDirectory( file );
88
      final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
89
90
      getWatchMap().put( key, dir );
91
    }
92
  }
93
94
  /**
95
   * Returns the given path to a file (or directory) as a directory. If the
96
   * given path is already a directory, it is returned. Otherwise, this returns
97
   * the directory that contains the file. This will fail if the file is stored
98
   * in the root folder.
99
   *
100
   * @param path The file to return as a directory, which should always be the
101
   *             case.
102
   * @return The given path as a directory, if a file, otherwise the path
103
   * itself.
104
   */
105
  private Path toDirectory( final Path path ) {
106
    return Files.isDirectory( path )
107
        ? path
108
        : path.toFile().getParentFile().toPath();
109
  }
110
111
  /**
112
   * Stop listening to the given file for change events. This fails silently.
113
   *
114
   * @param file The file to no longer monitor for changes.
115
   */
116
  @Override
117
  public void ignore( final Path file ) {
118
    if( file != null ) {
119
      final Path directory = toDirectory( file );
120
121
      // Remove all occurrences (there should be only one).
122
      getWatchMap().values().removeAll( Collections.singleton( directory ) );
123
124
      // Remove all occurrences (there can be only one).
125
      getEavesdropped().remove( file );
126
    }
127
  }
128
129
  /**
130
   * Loops until stop is called, or the application is terminated.
131
   */
132
  @Override
133
  @SuppressWarnings("BusyWait")
134
  public void run() {
135
    setListening( true );
136
137
    while( isListening() ) {
138
      try {
139
        final WatchKey key = getWatchService().take();
140
        final Path path = get( key );
141
142
        // Prevent receiving two separate ENTRY_MODIFY events: file modified
143
        // and timestamp updated. Instead, receive one ENTRY_MODIFY event
144
        // with two counts.
145
        Thread.sleep( APP_WATCHDOG_TIMEOUT );
146
147
        for( final WatchEvent<?> event : key.pollEvents() ) {
148
          final Path changed = path.resolve( (Path) event.context() );
149
150
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
151
            setChanged();
152
            notifyObservers( changed );
153
          }
154
        }
155
156
        if( !key.reset() ) {
157
          ignore( path );
158
        }
159
      } catch( final IOException | InterruptedException ex ) {
160
        // Stop eavesdropping.
161
        setListening( false );
162
      }
163
    }
164
  }
165
166
  /**
167
   * Returns true if the list of files being listened to for changes contains
168
   * the given file.
169
   *
170
   * @param file Path to a system file.
171
   * @return true The given file is being monitored for changes.
172
   */
173
  private boolean isListening( final Path file ) {
174
    return getEavesdropped().contains( file );
175
  }
176
177
  /**
178
   * Returns a path for a given watch key.
179
   *
180
   * @param key The key to lookup its corresponding path.
181
   * @return The path for the given key.
182
   */
183
  private Path get( final WatchKey key ) {
184
    return getWatchMap().get( key );
185
  }
186
187
  private synchronized Map<WatchKey, Path> getWatchMap() {
188
    if( this.keys == null ) {
189
      this.keys = createWatchKeys();
190
    }
191
192
    return this.keys;
193
  }
194
195
  protected Map<WatchKey, Path> createWatchKeys() {
196
    return new ConcurrentHashMap<>();
197
  }
198
199
  /**
200
   * Returns a list of files that, when changed, will kick off a notification.
201
   *
202
   * @return A non-null, possibly empty, list of files.
203
   */
204
  private synchronized Set<Path> getEavesdropped() {
205
    if( this.eavesdropped == null ) {
206
      this.eavesdropped = createEavesdropped();
207
    }
208
209
    return this.eavesdropped;
210
  }
211
212
  protected Set<Path> createEavesdropped() {
213
    return ConcurrentHashMap.newKeySet();
214
  }
215
216
  /**
217
   * The existing watch service, or a new instance if null.
218
   *
219
   * @return A valid WatchService instance, never null.
220
   * @throws IOException Could not create a new watch service.
221
   */
222
  private synchronized WatchService getWatchService() throws IOException {
223
    if( this.watchService == null ) {
224
      this.watchService = createWatchService();
225
    }
226
227
    return this.watchService;
228
  }
229
230
  protected WatchService createWatchService() throws IOException {
231
    final FileSystem fileSystem = FileSystems.getDefault();
232
    return fileSystem.newWatchService();
233
  }
234
235
  /**
236
   * Answers whether the loop should continue executing.
237
   *
238
   * @return true The internal listening loop should continue listening for file
239
   * modification events.
240
   */
241
  protected boolean isListening() {
242
    return this.listening;
243
  }
244
245
  /**
246
   * Requests the snitch to stop eavesdropping on file changes.
247
   *
248
   * @param listening Use true to indicate the service should stop running.
249
   */
250
  private void setListening( final boolean listening ) {
251
    this.listening = listening;
252
  }
253
}
2541
D src/main/java/com/scrivenvar/sigils/RSigilOperator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.sigils;
29
30
import static com.scrivenvar.sigils.YamlSigilOperator.KEY_SEPARATOR_DEF;
31
32
/**
33
 * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils.
34
 */
35
public class RSigilOperator extends SigilOperator {
36
  public static final char KEY_SEPARATOR_R = '$';
37
38
  public static final String PREFIX = "`r#";
39
  public static final char SUFFIX = '`';
40
41
  private final String mDelimiterBegan =
42
      getUserPreferences().getRDelimiterBegan();
43
  private final String mDelimiterEnded =
44
      getUserPreferences().getRDelimiterEnded();
45
46
  /**
47
   * Returns the given string R-escaping backticks prepended and appended. This
48
   * is not null safe. Do not pass null into this method.
49
   *
50
   * @param key The string to adorn with R token delimiters.
51
   * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`".
52
   */
53
  @Override
54
  public String apply( final String key ) {
55
    assert key != null;
56
57
    return PREFIX
58
        + mDelimiterBegan
59
        + entoken( key )
60
        + mDelimiterEnded
61
        + SUFFIX;
62
  }
63
64
  /**
65
   * Transforms a definition key (bracketed by token delimiters) into the
66
   * expected format for an R variable key name.
67
   *
68
   * @param key The variable name to transform, can be empty but not null.
69
   * @return The transformed variable name.
70
   */
71
  public static String entoken( final String key ) {
72
    return "v$" +
73
        YamlSigilOperator.detoken( key )
74
                         .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R );
75
  }
76
}
771
D src/main/java/com/scrivenvar/sigils/SigilOperator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.sigils;
29
30
import com.scrivenvar.preferences.UserPreferences;
31
32
import java.util.function.UnaryOperator;
33
34
/**
35
 * Responsible for updating definition keys to use a machine-readable format
36
 * corresponding to the type of file being edited. This changes a definition
37
 * key name based on some criteria determined by the factory that creates
38
 * implementations of this interface.
39
 */
40
public abstract class SigilOperator implements UnaryOperator<String> {
41
  protected static UserPreferences getUserPreferences() {
42
    return UserPreferences.getInstance();
43
  }
44
}
451
D src/main/java/com/scrivenvar/sigils/YamlSigilOperator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.sigils;
29
30
import java.util.regex.Pattern;
31
32
import static java.lang.String.format;
33
import static java.util.regex.Pattern.compile;
34
import static java.util.regex.Pattern.quote;
35
36
/**
37
 * Brackets definition keys with token delimiters.
38
 */
39
public class YamlSigilOperator extends SigilOperator {
40
  public static final char KEY_SEPARATOR_DEF = '.';
41
42
  private static final String mDelimiterBegan =
43
      getUserPreferences().getDefDelimiterBegan();
44
  private static final String mDelimiterEnded =
45
      getUserPreferences().getDefDelimiterEnded();
46
47
  /**
48
   * Non-greedy match of key names delimited by definition tokens.
49
   */
50
  private static final String REGEX =
51
      format( "(%s.*?%s)", quote( mDelimiterBegan ), quote( mDelimiterEnded ) );
52
53
  /**
54
   * Compiled regular expression for matching delimited references.
55
   */
56
  public static final Pattern REGEX_PATTERN = compile( REGEX );
57
58
  /**
59
   * Returns the given {@link String} verbatim because variables in YAML
60
   * documents and plain Markdown documents already have the appropriate
61
   * tokenizable syntax wrapped around the text.
62
   *
63
   * @param key Returned verbatim.
64
   */
65
  @Override
66
  public String apply( final String key ) {
67
    return key;
68
  }
69
70
  /**
71
   * Adds delimiters to the given key.
72
   *
73
   * @param key The key to adorn with start and stop definition tokens.
74
   * @return The given key bracketed by definition token symbols.
75
   */
76
  public static String entoken( final String key ) {
77
    assert key != null;
78
    return mDelimiterBegan + key + mDelimiterEnded;
79
  }
80
81
  /**
82
   * Removes start and stop definition key delimiters from the given key. This
83
   * method does not check for delimiters, only that there are sufficient
84
   * characters to remove from either end of the given key.
85
   *
86
   * @param key The key adorned with start and stop definition tokens.
87
   * @return The given key with the delimiters removed.
88
   */
89
  public static String detoken( final String key ) {
90
    final int beganLen = mDelimiterBegan.length();
91
    final int endedLen = mDelimiterEnded.length();
92
93
    return key.length() > beganLen + endedLen
94
        ? key.substring( beganLen, key.length() - endedLen )
95
        : key;
96
  }
97
}
981
D src/main/java/com/scrivenvar/spelling/api/SpellCheckListener.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.spelling.api;
29
30
import java.util.function.BiConsumer;
31
32
/**
33
 * Represents an operation that accepts two input arguments and returns no
34
 * result. Unlike most other functional interfaces, this class is expected to
35
 * operate via side-effects.
36
 * <p>
37
 * This is used instead of a {@link BiConsumer} to avoid autoboxing.
38
 * </p>
39
 */
40
@FunctionalInterface
41
public interface SpellCheckListener {
42
43
  /**
44
   * Performs an operation on the given arguments.
45
   *
46
   * @param text        The text associated with a beginning and ending offset.
47
   * @param beganOffset A starting offset, used as an index into a string.
48
   * @param endedOffset An ending offset, which should equal text.length() +
49
   *                    beganOffset.
50
   */
51
  void accept( String text, int beganOffset, int endedOffset );
52
}
531
D src/main/java/com/scrivenvar/spelling/api/SpellChecker.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.spelling.api;
29
30
import java.util.List;
31
32
/**
33
 * Defines the responsibilities for a spell checking API. The intention is
34
 * to allow different spell checking implementations to be used by the
35
 * application, such as SymSpell and LinSpell.
36
 */
37
public interface SpellChecker {
38
39
  /**
40
   * Answers whether the given lexeme, in whole, is found in the lexicon. The
41
   * lexicon lookup is performed case-insensitively. This method should be
42
   * used instead of {@link #suggestions(String, int)} for performance reasons.
43
   *
44
   * @param lexeme The word to check for correctness.
45
   * @return {@code true} if the lexeme is in the lexicon.
46
   */
47
  boolean inLexicon( String lexeme );
48
49
  /**
50
   * Gets a list of spelling corrections for the given lexeme.
51
   *
52
   * @param lexeme A word to check for correctness that's not in the lexicon.
53
   * @param count  The maximum number of alternatives to return.
54
   * @return A list of words in the lexicon that are similar to the given
55
   * lexeme.
56
   */
57
  List<String> suggestions( String lexeme, int count );
58
59
  /**
60
   * Iterates over the given text, emitting starting and ending offsets into
61
   * the text for every word that is missing from the lexicon.
62
   *
63
   * @param text     The text to check for words missing from the lexicon.
64
   * @param consumer Every missing word emits a message with the starting
65
   *                 and ending offset into the text where said word is found.
66
   */
67
  void proofread( String text, SpellCheckListener consumer );
68
}
691
D src/main/java/com/scrivenvar/spelling/impl/PermissiveSpeller.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.spelling.impl;
29
30
import com.scrivenvar.spelling.api.SpellCheckListener;
31
import com.scrivenvar.spelling.api.SpellChecker;
32
33
import java.util.List;
34
35
/**
36
 * Responsible for spell checking in the event that a real spell checking
37
 * implementation cannot be created (for any reason). Does not perform any
38
 * spell checking and indicates that any given lexeme is in the lexicon.
39
 */
40
public class PermissiveSpeller implements SpellChecker {
41
  /**
42
   * Returns {@code true}, ignoring the given word.
43
   *
44
   * @param ignored Unused.
45
   * @return {@code true}
46
   */
47
  @Override
48
  public boolean inLexicon( final String ignored ) {
49
    return true;
50
  }
51
52
  /**
53
   * Returns an array with the given lexeme.
54
   *
55
   * @param lexeme  The word to return.
56
   * @param ignored Unused.
57
   * @return A suggestion list containing the given lexeme.
58
   */
59
  @Override
60
  public List<String> suggestions( final String lexeme, final int ignored ) {
61
    return List.of( lexeme );
62
  }
63
64
  /**
65
   * Performs no action.
66
   *
67
   * @param text    Unused.
68
   * @param ignored Uncalled.
69
   */
70
  @Override
71
  public void proofread(
72
      final String text, final SpellCheckListener ignored ) {
73
  }
74
}
751
D src/main/java/com/scrivenvar/spelling/impl/SymSpellSpeller.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.spelling.impl;
29
30
import com.scrivenvar.spelling.api.SpellCheckListener;
31
import com.scrivenvar.spelling.api.SpellChecker;
32
import io.gitlab.rxp90.jsymspell.SuggestItem;
33
import io.gitlab.rxp90.jsymspell.SymSpell;
34
import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
35
36
import java.text.BreakIterator;
37
import java.util.ArrayList;
38
import java.util.Collection;
39
import java.util.List;
40
41
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
42
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
43
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
44
import static java.lang.Character.isLetter;
45
46
/**
47
 * Responsible for spell checking using {@link SymSpell}.
48
 */
49
public class SymSpellSpeller implements SpellChecker {
50
  private final BreakIterator mBreakIterator = BreakIterator.getWordInstance();
51
52
  private final SymSpell mSymSpell;
53
54
  /**
55
   * Creates a new lexicon for the given collection of lexemes.
56
   *
57
   * @param lexiconWords The words in the lexicon to add for spell checking,
58
   *                     must not be empty.
59
   * @return An instance of {@link SpellChecker} that can check if a word
60
   * is correct and suggest alternatives.
61
   */
62
  public static SpellChecker forLexicon(
63
      final Collection<String> lexiconWords ) {
64
    assert lexiconWords != null && !lexiconWords.isEmpty();
65
66
    final SymSpellBuilder builder = new SymSpellBuilder()
67
        .setLexiconWords( lexiconWords );
68
69
    return new SymSpellSpeller( builder.build() );
70
  }
71
72
  /**
73
   * Prevent direct instantiation so that only the {@link SpellChecker}
74
   * interface
75
   * is available.
76
   *
77
   * @param symSpell The implementation-specific spell checker.
78
   */
79
  private SymSpellSpeller( final SymSpell symSpell ) {
80
    mSymSpell = symSpell;
81
  }
82
83
  @Override
84
  public boolean inLexicon( final String lexeme ) {
85
    return lookup( lexeme, CLOSEST ).size() == 1;
86
  }
87
88
  @Override
89
  public List<String> suggestions( final String lexeme, int count ) {
90
    final List<String> result = new ArrayList<>( count );
91
92
    for( final var item : lookup( lexeme, ALL ) ) {
93
      if( count-- > 0 ) {
94
        result.add( item.getSuggestion() );
95
      }
96
      else {
97
        break;
98
      }
99
    }
100
101
    return result;
102
  }
103
104
  @Override
105
  public void proofread(
106
      final String text, final SpellCheckListener consumer ) {
107
    assert text != null;
108
    assert consumer != null;
109
110
    mBreakIterator.setText( text );
111
112
    int boundaryIndex = mBreakIterator.first();
113
    int previousIndex = 0;
114
115
    while( boundaryIndex != BreakIterator.DONE ) {
116
      final var lex = text.substring( previousIndex, boundaryIndex )
117
                          .toLowerCase();
118
119
      // Get the lexeme for the possessive.
120
      final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" );
121
      final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex;
122
123
      if( isWord( lexeme ) && !inLexicon( lexeme ) ) {
124
        consumer.accept( lex, previousIndex, boundaryIndex );
125
      }
126
127
      previousIndex = boundaryIndex;
128
      boundaryIndex = mBreakIterator.next();
129
    }
130
  }
131
132
  /**
133
   * Answers whether the given string is likely a word by checking the first
134
   * character.
135
   *
136
   * @param word The word to check.
137
   * @return {@code true} if the word begins with a letter.
138
   */
139
  private boolean isWord( final String word ) {
140
    return !word.isEmpty() && isLetter( word.charAt( 0 ) );
141
  }
142
143
  /**
144
   * Returns a list of {@link SuggestItem} instances that provide alternative
145
   * spellings for the given lexeme.
146
   *
147
   * @param lexeme A word to look up in the lexicon.
148
   * @param v      Influences the number of results returned.
149
   * @return Alternative lexemes.
150
   */
151
  private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
152
    return getSpeller().lookup( lexeme, v );
153
  }
154
155
  private SymSpell getSpeller() {
156
    return mSymSpell;
157
  }
158
}
1591
D src/main/java/com/scrivenvar/util/Action.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import de.jensd.fx.glyphs.GlyphIcons;
30
import javafx.beans.value.ObservableBooleanValue;
31
import javafx.event.ActionEvent;
32
import javafx.event.EventHandler;
33
import javafx.scene.input.KeyCombination;
34
35
/**
36
 * Defines actions the user can take by interacting with the GUI.
37
 */
38
public class Action {
39
  public final String text;
40
  public final KeyCombination accelerator;
41
  public final GlyphIcons icon;
42
  public final EventHandler<ActionEvent> action;
43
  public final ObservableBooleanValue disable;
44
45
  public Action(
46
      final String text,
47
      final String accelerator,
48
      final GlyphIcons icon,
49
      final EventHandler<ActionEvent> action,
50
      final ObservableBooleanValue disable ) {
51
52
    this.text = text;
53
    this.accelerator = accelerator == null ?
54
        null : KeyCombination.valueOf( accelerator );
55
    this.icon = icon;
56
    this.action = action;
57
    this.disable = disable;
58
  }
59
}
601
D src/main/java/com/scrivenvar/util/ActionBuilder.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
import com.scrivenvar.Messages;
31
import de.jensd.fx.glyphs.GlyphIcons;
32
import javafx.beans.value.ObservableBooleanValue;
33
import javafx.event.ActionEvent;
34
import javafx.event.EventHandler;
35
36
/**
37
 * Provides a fluent interface around constructing actions so that duplication
38
 * can be avoided.
39
 */
40
public class ActionBuilder {
41
  private String mText;
42
  private String mAccelerator;
43
  private GlyphIcons mIcon;
44
  private EventHandler<ActionEvent> mAction;
45
  private ObservableBooleanValue mDisable;
46
47
  /**
48
   * Sets the action text based on a resource bundle key.
49
   *
50
   * @param key The key to look up in the {@link Messages}.
51
   * @return The corresponding value, or the key name if none found.
52
   */
53
  public ActionBuilder setText( final String key ) {
54
    mText = Messages.get( key, key );
55
    return this;
56
  }
57
58
  public ActionBuilder setAccelerator( final String accelerator ) {
59
    mAccelerator = accelerator;
60
    return this;
61
  }
62
63
  public ActionBuilder setIcon( final GlyphIcons icon ) {
64
    mIcon = icon;
65
    return this;
66
  }
67
68
  public ActionBuilder setAction( final EventHandler<ActionEvent> action ) {
69
    mAction = action;
70
    return this;
71
  }
72
73
  public ActionBuilder setDisable( final ObservableBooleanValue disable ) {
74
    mDisable = disable;
75
    return this;
76
  }
77
78
  public Action build() {
79
    return new Action( mText, mAccelerator, mIcon, mAction, mDisable );
80
  }
81
}
821
D src/main/java/com/scrivenvar/util/ActionUtils.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
30
import javafx.scene.Node;
31
import javafx.scene.control.Button;
32
import javafx.scene.control.Menu;
33
import javafx.scene.control.MenuItem;
34
import javafx.scene.control.Separator;
35
import javafx.scene.control.SeparatorMenuItem;
36
import javafx.scene.control.ToolBar;
37
import javafx.scene.control.Tooltip;
38
39
/**
40
 * Responsible for creating menu items and toolbar buttons.
41
 */
42
public class ActionUtils {
43
44
  public static Menu createMenu( final String text, final Action... actions ) {
45
    return new Menu( text, null, createMenuItems( actions ) );
46
  }
47
48
  public static MenuItem[] createMenuItems( final Action... actions ) {
49
    final MenuItem[] menuItems = new MenuItem[ actions.length ];
50
51
    for( int i = 0; i < actions.length; i++ ) {
52
      menuItems[ i ] = (actions[ i ] == null)
53
          ? new SeparatorMenuItem()
54
          : createMenuItem( actions[ i ] );
55
    }
56
57
    return menuItems;
58
  }
59
60
  public static MenuItem createMenuItem( final Action action ) {
61
    final MenuItem menuItem = new MenuItem( action.text );
62
63
    if( action.accelerator != null ) {
64
      menuItem.setAccelerator( action.accelerator );
65
    }
66
67
    if( action.icon != null ) {
68
      menuItem.setGraphic(
69
          FontAwesomeIconFactory.get().createIcon( action.icon ) );
70
    }
71
72
    menuItem.setOnAction( action.action );
73
74
    if( action.disable != null ) {
75
      menuItem.disableProperty().bind( action.disable );
76
    }
77
78
    menuItem.setMnemonicParsing( true );
79
80
    return menuItem;
81
  }
82
83
  public static ToolBar createToolBar( final Action... actions ) {
84
    return new ToolBar( createToolBarButtons( actions ) );
85
  }
86
87
  public static Node[] createToolBarButtons( final Action... actions ) {
88
    Node[] buttons = new Node[ actions.length ];
89
    for( int i = 0; i < actions.length; i++ ) {
90
      buttons[ i ] = (actions[ i ] != null)
91
          ? createToolBarButton( actions[ i ] )
92
          : new Separator();
93
    }
94
    return buttons;
95
  }
96
97
  public static Button createToolBarButton( final Action action ) {
98
    final Button button = new Button();
99
    button.setGraphic(
100
        FontAwesomeIconFactory
101
            .get()
102
            .createIcon( action.icon, "1.2em" ) );
103
104
    String tooltip = action.text;
105
106
    if( tooltip.endsWith( "..." ) ) {
107
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
108
    }
109
110
    if( action.accelerator != null ) {
111
      tooltip += " (" + action.accelerator.getDisplayText() + ')';
112
    }
113
114
    button.setTooltip( new Tooltip( tooltip ) );
115
    button.setFocusTraversable( false );
116
    button.setOnAction( action.action );
117
118
    if( action.disable != null ) {
119
      button.disableProperty().bind( action.disable );
120
    }
121
122
    return button;
123
  }
124
}
1251
D src/main/java/com/scrivenvar/util/BoundedCache.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
import java.util.LinkedHashMap;
31
import java.util.Map;
32
33
/**
34
 * A map that removes the oldest entry once its capacity (cache size) has
35
 * been reached.
36
 *
37
 * @param <K> The type of key mapped to a value.
38
 * @param <V> The type of value mapped to a key.
39
 */
40
public class BoundedCache<K, V> extends LinkedHashMap<K, V> {
41
  private final int mCacheSize;
42
43
  /**
44
   * Constructs a new instance having a finite size.
45
   *
46
   * @param cacheSize The maximum number of entries.
47
   */
48
  public BoundedCache( final int cacheSize ) {
49
    mCacheSize = cacheSize;
50
  }
51
52
  @Override
53
  protected boolean removeEldestEntry(
54
      final Map.Entry<K, V> eldest ) {
55
    return size() > mCacheSize;
56
  }
57
}
581
D src/main/java/com/scrivenvar/util/ProtocolResolver.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
import java.io.File;
31
import java.net.MalformedURLException;
32
import java.net.URI;
33
import java.net.URL;
34
35
import static com.scrivenvar.util.ProtocolScheme.UNKNOWN;
36
37
/**
38
 * Responsible for determining the protocol of a resource.
39
 */
40
public class ProtocolResolver {
41
  /**
42
   * Returns the protocol for a given URI or filename.
43
   *
44
   * @param resource Determine the protocol for this URI or filename.
45
   * @return The protocol for the given resource.
46
   */
47
  public static ProtocolScheme getProtocol( final String resource ) {
48
    String protocol;
49
50
    try {
51
      final URI uri = new URI( resource );
52
53
      if( uri.isAbsolute() ) {
54
        protocol = uri.getScheme();
55
      }
56
      else {
57
        final URL url = new URL( resource );
58
        protocol = url.getProtocol();
59
      }
60
    } catch( final Exception e ) {
61
      // Could be HTTP, HTTPS?
62
      if( resource.startsWith( "//" ) ) {
63
        throw new IllegalArgumentException( "Relative context: " + resource );
64
      }
65
      else {
66
        final File file = new File( resource );
67
        protocol = getProtocol( file );
68
      }
69
    }
70
71
    return ProtocolScheme.valueFrom( protocol );
72
  }
73
74
  /**
75
   * Returns the protocol for a given file.
76
   *
77
   * @param file Determine the protocol for this file.
78
   * @return The protocol for the given file.
79
   */
80
  private static String getProtocol( final File file ) {
81
    String result;
82
83
    try {
84
      result = file.toURI().toURL().getProtocol();
85
    } catch( final MalformedURLException ex ) {
86
      // Value guaranteed to avoid identification as a standard protocol.
87
      result = UNKNOWN.toString();
88
    }
89
90
    return result;
91
  }
92
}
931
D src/main/java/com/scrivenvar/util/ProtocolScheme.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
/**
31
 * Represents the type of data encoding scheme used for a universal resource
32
 * indicator.
33
 */
34
public enum ProtocolScheme {
35
  /**
36
   * Denotes either HTTP or HTTPS.
37
   */
38
  HTTP,
39
  /**
40
   * Denotes a local file.
41
   */
42
  FILE,
43
  /**
44
   * Could not determine schema (or is not supported by the application).
45
   */
46
  UNKNOWN;
47
48
  /**
49
   * Answers {@code true} if the given protocol is either HTTP or HTTPS.
50
   *
51
   * @return {@code true} the protocol is either HTTP or HTTPS.
52
   */
53
  public boolean isHttp() {
54
    return this == HTTP;
55
  }
56
57
  /**
58
   * Answers {@code true} if the given protocol is for a local file.
59
   *
60
   * @return {@code true} the protocol is for a local file reference.
61
   */
62
  public boolean isFile() {
63
    return this == FILE;
64
  }
65
66
  /**
67
   * Determines the protocol scheme for a given string.
68
   *
69
   * @param protocol A string representing data encoding protocol scheme.
70
   * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
71
   * valid value from this enumeration.
72
   */
73
  public static ProtocolScheme valueFrom( String protocol ) {
74
    ProtocolScheme result = UNKNOWN;
75
    protocol = sanitize( protocol );
76
77
    for( final var scheme : values() ) {
78
      // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate.
79
      if( protocol.startsWith( scheme.name() ) ) {
80
        result = scheme;
81
        break;
82
      }
83
    }
84
85
    return result;
86
  }
87
88
  /**
89
   * Returns an empty string if the given string to sanitize is {@code null},
90
   * otherwise the given string in uppercase. Uppercase is used to align with
91
   * the enum name.
92
   *
93
   * @param s The string to sanitize, may be {@code null}.
94
   * @return A non-{@code null} string.
95
   */
96
  private static String sanitize( final String s ) {
97
    return s == null ? "" : s.toUpperCase();
98
  }
99
}
1001
D src/main/java/com/scrivenvar/util/ResourceWalker.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
import java.io.IOException;
31
import java.net.URISyntaxException;
32
import java.nio.file.*;
33
import java.util.function.Consumer;
34
35
import static java.nio.file.FileSystems.newFileSystem;
36
import static java.util.Collections.emptyMap;
37
38
/**
39
 * Responsible for finding file resources.
40
 */
41
public class ResourceWalker {
42
  private static final PathMatcher PATH_MATCHER =
43
      FileSystems.getDefault().getPathMatcher( "glob:**.{ttf,otf}" );
44
45
  /**
46
   * @param dirName The root directory to scan for files matching the glob.
47
   * @param c       The consumer function to call for each matching path found.
48
   * @throws URISyntaxException Could not convert the resource to a URI.
49
   * @throws IOException        Could not walk the tree.
50
   */
51
  public static void walk( final String dirName, final Consumer<Path> c )
52
      throws URISyntaxException, IOException {
53
    final var resource = ResourceWalker.class.getResource( dirName );
54
55
    if( resource != null ) {
56
      final var uri = resource.toURI();
57
      final var path = uri.getScheme().equals( "jar" )
58
          ? newFileSystem( uri, emptyMap() ).getPath( dirName )
59
          : Paths.get( uri );
60
      final var walk = Files.walk( path, 10 );
61
62
      for( final var it = walk.iterator(); it.hasNext(); ) {
63
        final Path p = it.next();
64
        if( PATH_MATCHER.matches( p ) ) {
65
          c.accept( p );
66
        }
67
      }
68
    }
69
  }
70
}
711
D src/main/java/com/scrivenvar/util/StageState.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import java.util.prefs.Preferences;
30
31
import javafx.application.Platform;
32
import javafx.scene.shape.Rectangle;
33
import javafx.stage.Stage;
34
import javafx.stage.WindowEvent;
35
36
/**
37
 * Saves and restores Stage state (window bounds, maximized, fullScreen).
38
 */
39
public class StageState {
40
41
  public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
42
  public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
43
  public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
44
45
  private final Stage mStage;
46
  private final Preferences mState;
47
48
  private Rectangle normalBounds;
49
  private boolean runLaterPending;
50
51
  public StageState( final Stage stage, final Preferences state ) {
52
    mStage = stage;
53
    mState = state;
54
55
    restore();
56
57
    stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
58
59
    stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() );
60
    stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() );
61
    stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() );
62
    stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() );
63
  }
64
65
  private void save() {
66
    final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
67
68
    if( bounds != null ) {
69
      mState.putDouble( "windowX", bounds.getX() );
70
      mState.putDouble( "windowY", bounds.getY() );
71
      mState.putDouble( "windowWidth", bounds.getWidth() );
72
      mState.putDouble( "windowHeight", bounds.getHeight() );
73
    }
74
75
    mState.putBoolean( "windowMaximized", mStage.isMaximized() );
76
    mState.putBoolean( "windowFullScreen", mStage.isFullScreen() );
77
  }
78
79
  private void restore() {
80
    final double x = mState.getDouble( "windowX", Double.NaN );
81
    final double y = mState.getDouble( "windowY", Double.NaN );
82
    final double w = mState.getDouble( "windowWidth", Double.NaN );
83
    final double h = mState.getDouble( "windowHeight", Double.NaN );
84
    final boolean maximized = mState.getBoolean( "windowMaximized", false );
85
    final boolean fullScreen = mState.getBoolean( "windowFullScreen", false );
86
87
    if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
88
      mStage.setX( x );
89
      mStage.setY( y );
90
    } // else: default behavior is center on screen
91
92
    if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
93
      mStage.setWidth( w );
94
      mStage.setHeight( h );
95
    } // else: default behavior is use scene size
96
97
    if( fullScreen != mStage.isFullScreen() ) {
98
      mStage.setFullScreen( fullScreen );
99
    }
100
101
    if( maximized != mStage.isMaximized() ) {
102
      mStage.setMaximized( maximized );
103
    }
104
  }
105
106
  /**
107
   * Remembers the window bounds when the window is not iconified, maximized or
108
   * in fullScreen.
109
   */
110
  private void boundsChanged() {
111
    // avoid too many (and useless) runLater() invocations
112
    if( runLaterPending ) {
113
      return;
114
    }
115
116
    runLaterPending = true;
117
118
    // must use runLater() to ensure that change of all properties
119
    // (x, y, width, height, iconified, maximized and fullScreen)
120
    // has finished
121
    Platform.runLater( () -> {
122
      runLaterPending = false;
123
124
      if( isNormalState() ) {
125
        normalBounds = getStageBounds();
126
      }
127
    } );
128
  }
129
130
  private boolean isNormalState() {
131
    return !mStage.isIconified() &&
132
        !mStage.isMaximized() &&
133
        !mStage.isFullScreen();
134
  }
135
136
  private Rectangle getStageBounds() {
137
    return new Rectangle(
138
        mStage.getX(),
139
        mStage.getY(),
140
        mStage.getWidth(),
141
        mStage.getHeight()
142
    );
143
  }
144
}
1451
D src/main/java/com/scrivenvar/util/Utils.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import java.util.ArrayList;
30
import java.util.prefs.Preferences;
31
32
/**
33
 * Responsible for trimming, storing, and retrieving strings.
34
 */
35
public class Utils {
36
37
  public static String ltrim( final String s ) {
38
    int i = 0;
39
40
    while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) {
41
      i++;
42
    }
43
44
    return s.substring( i );
45
  }
46
47
  public static String rtrim( final String s ) {
48
    int i = s.length() - 1;
49
50
    while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) {
51
      i--;
52
    }
53
54
    return s.substring( 0, i + 1 );
55
  }
56
57
  public static String[] getPrefsStrings( final Preferences prefs,
58
                                          String key ) {
59
    final ArrayList<String> arr = new ArrayList<>( 256 );
60
61
    for( int i = 0; i < 10000; i++ ) {
62
      final String s = prefs.get( key + (i + 1), null );
63
64
      if( s == null ) {
65
        break;
66
      }
67
68
      arr.add( s );
69
    }
70
71
    return arr.toArray( new String[ 0 ] );
72
  }
73
74
  public static void putPrefsStrings( Preferences prefs, String key,
75
                                      String[] strings ) {
76
    for( int i = 0; i < strings.length; i++ ) {
77
      prefs.put( key + (i + 1), strings[ i ] );
78
    }
79
80
    for( int i = strings.length; prefs.get( key + (i + 1),
81
                                            null ) != null; i++ ) {
82
      prefs.remove( key + (i + 1) );
83
    }
84
  }
85
}
861
A src/main/resources/META-INF/services/com.keenwrite.service.Options
1
1
com.keenwrite.service.impl.DefaultOptions
A src/main/resources/META-INF/services/com.keenwrite.service.Settings
1
1
com.keenwrite.service.impl.DefaultSettings
A src/main/resources/META-INF/services/com.keenwrite.service.Snitch
1
1
com.keenwrite.service.impl.DefaultSnitch
A src/main/resources/META-INF/services/com.keenwrite.service.events.Notifier
1
1
com.keenwrite.service.events.impl.DefaultNotifier
D src/main/resources/META-INF/services/com.scrivenvar.service.Options
1
com.scrivenvar.service.impl.DefaultOptions
1
D src/main/resources/META-INF/services/com.scrivenvar.service.Settings
1
com.scrivenvar.service.impl.DefaultSettings
1
D src/main/resources/META-INF/services/com.scrivenvar.service.Snitch
1
com.scrivenvar.service.impl.DefaultSnitch
1
D src/main/resources/META-INF/services/com.scrivenvar.service.events.Notifier
1
com.scrivenvar.service.events.impl.DefaultNotifier
1
A src/main/resources/bootstrap.properties
1
# Used by the Gradle build script and the application.
2
application.title=KeenWrite
3
14
A src/main/resources/com/keenwrite/.gitignore
1
app.properties
12
A src/main/resources/com/keenwrite/app-title.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   version="1.1"
11
   width="971.53119"
12
   height="498.39355"
13
   viewBox="0 0 971.53119 498.39354"
14
   xml:space="preserve"
15
   id="svg52"
16
   sodipodi:docname="app-title.svg"
17
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
18
   inkscape:export-filename="/home/jarvisd/dev/java/scrivenvar/docs/images/app-title.png"
19
   inkscape:export-xdpi="24.66"
20
   inkscape:export-ydpi="24.66"><metadata
21
   id="metadata56"><rdf:RDF><cc:Work
22
       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
23
         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview
24
   inkscape:document-rotation="0"
25
   pagecolor="#ffffff"
26
   bordercolor="#666666"
27
   borderopacity="1"
28
   objecttolerance="10"
29
   gridtolerance="10"
30
   guidetolerance="10"
31
   inkscape:pageopacity="0"
32
   inkscape:pageshadow="2"
33
   inkscape:window-width="640"
34
   inkscape:window-height="480"
35
   id="namedview54"
36
   showgrid="false"
37
   inkscape:zoom="0.78417969"
38
   inkscape:cx="455.5775"
39
   inkscape:cy="347.59625"
40
   inkscape:current-layer="svg52"
41
   fit-margin-top="0"
42
   fit-margin-left="0"
43
   fit-margin-right="0"
44
   fit-margin-bottom="0" />
45
<desc
46
   id="desc2">Created with Fabric.js 3.6.3</desc>
47
<defs
48
   id="defs4"><rect
49
   x="114.92139"
50
   y="132.06313"
51
   width="470.12033"
52
   height="175.55823"
53
   id="rect933" />
54
55
56
57
		
58
		
59
		
60
		
61
		
62
		
63
		
64
		
65
66
<linearGradient
67
   y2="-0.049471263"
68
   x2="0.96880889"
69
   y1="-0.044911571"
70
   x1="0.15235768"
71
   gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
72
   gradientUnits="userSpaceOnUse"
73
   id="SVGID_1_302284">
74
<stop
75
   id="stop9"
76
   style="stop-color:#ec706a;stop-opacity:1"
77
   offset="0%" />
78
<stop
79
   id="stop11"
80
   style="stop-color:#ecd980;stop-opacity:1"
81
   offset="100%" />
82
</linearGradient>
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
</defs>
99
100
<g
101
   id="g853"
102
   transform="translate(-394.35834,-171.20491)"><path
103
     style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
104
     paint-order="stroke"
105
     d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534"
106
     stroke-linecap="round"
107
     id="path14" /><path
108
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
109
     paint-order="stroke"
110
     d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
111
     stroke-linecap="round"
112
     id="path22" /><path
113
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
114
     paint-order="stroke"
115
     d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
116
     stroke-linecap="round"
117
     id="path26" /><path
118
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
119
     paint-order="stroke"
120
     d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z"
121
     stroke-linecap="round"
122
     id="path30" /><path
123
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
124
     paint-order="stroke"
125
     d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
126
     stroke-linecap="round"
127
     id="path34" /><path
128
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
129
     paint-order="stroke"
130
     d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
131
     stroke-linecap="round"
132
     id="path38" /><path
133
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
134
     paint-order="stroke"
135
     d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508"
136
     stroke-linecap="round"
137
     id="path42" /></g>
138
139
<text
140
   xml:space="preserve"
141
   id="text931"
142
   style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);fill:#000000;fill-opacity:1;stroke:none;"
143
   transform="translate(-394.35834,-171.20491)" /><text
144
   xml:space="preserve"
145
   style="font-style:italic;font-variant:normal;font-weight:800;font-stretch:normal;font-size:133.333px;line-height:1.25;font-family:'Merriweather Sans';-inkscape-font-specification:'Merriweather Sans, Ultra-Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#51a9cf;fill-opacity:1;stroke:none"
146
   x="311.66693"
147
   y="402.20627"
148
   id="text939"><tspan
149
     sodipodi:role="line"
150
     id="tspan937"
151
     x="311.66693"
152
     y="402.20627">KeenWrite</tspan></text></svg>
1153
A src/main/resources/com/keenwrite/build.sh
1
#!/bin/bash
2
3
INKSCAPE="/usr/bin/inkscape"
4
PNG_COMPRESS="optipng"
5
PNG_COMPRESS_OPTS="-o9 *png"
6
ICO_TOOL="icotool"
7
ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
8
9
declare -a SIZES=("16" "32" "64" "128" "256" "512")
10
11
for i in "${SIZES[@]}"; do
12
  # -y: export background opacity 0
13
  $INKSCAPE -y 0 -w "${i}" --export-overwrite --export-type=png -o "logo${i}.png" "logo.svg" 
14
done
15
16
# Compess the PNG images.
17
which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
18
19
# Generate an ICO file.
20
which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
21
122
A src/main/resources/com/keenwrite/editor/markdown.css
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
29
.markdown-editor {
30
  -fx-font-size: 11pt;
31
}
32
33
/* Subtly highlight the current paragraph. */
34
.markdown-editor .paragraph-box:has-caret {
35
  -fx-background-color: #fcfeff;
36
}
37
38
/* Light colour for selection highlight. */
39
.markdown-editor .selection {
40
  -fx-fill: #a6d2ff;
41
}
42
43
/* Decoration for words not found in the lexicon. */
44
.markdown-editor .spelling {
45
  -rtfx-underline-color: rgba(255, 131, 67, .7);
46
  -rtfx-underline-dash-array: 4, 2;
47
  -rtfx-underline-width: 2;
48
  -rtfx-underline-cap: round;
49
}
150
A src/main/resources/com/keenwrite/logo-original.svg
1
1
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
2
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1280" height="1024" viewBox="0 0 1280 1024" xml:space="preserve">
4
<desc>Created with Fabric.js 3.6.3</desc>
5
<defs>
6
</defs>
7
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0153846153846 512.012312418764)" id="background-logo"  >
8
<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  x="-325" y="-260" rx="0" ry="0" width="650" height="520" />
9
</g>
10
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0170725174504 420.4016715831266)" id="logo-logo"  >
11
<g style=""  paint-order="stroke"   >
12
		<g transform="matrix(2.537 0 0 -2.537 -86.35385711719567 85.244912)"  >
13
<linearGradient id="SVGID_1_302284" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-24.348526 -27.478867 -27.478867 24.348526 138.479 129.67187)"  x1="0" y1="0" x2="1" y2="0">
14
<stop offset="0%" style="stop-color:rgb(245,132,41);stop-opacity: 1"/>
15
<stop offset="100%" style="stop-color:rgb(251,173,23);stop-opacity: 1"/>
16
</linearGradient>
17
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#SVGID_1_302284); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-127.92674550729492, -117.16399999999999)" d="m 118.951 124.648 c -9.395 -14.441 -5.243 -20.693 -5.243 -20.693 v 0 c 0 0 6.219 9.126 9.771 5.599 v 0 c 3.051 -3.023 -2.415 -8.668 -2.415 -8.668 v 0 c 0 0 33.24 13.698 17.995 28.872 v 0 c 0 0 -3.203 3.683 -7.932 3.684 v 0 c -3.46 0 -7.736 -1.97 -12.176 -8.794" stroke-linecap="round" />
18
</g>
19
		<g transform="matrix(2.537 0 0 -2.537 -84.52085711719567 70.2729119999999)"  >
20
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(250,220,153); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(11.9895, -1.2609990716440347)" d="m 0 0 c 0 0 -6.501 6.719 -11.093 5.443 c -5.584 -1.545 -12.886 -12.078 -12.886 -12.078 c 0 0 5.98 16.932 15.29 15.731 C -1.19 8.127 0 0 0 0" stroke-linecap="round" />
21
</g>
22
		<g transform="matrix(2.537 0 0 -2.537 -22.327857117195663 48.729911999999956)"  >
23
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-4.189, -10.432)" d="m 0 0 l -0.87 16.89 l 3.995 3.974 l 6.123 -6.156 z" stroke-linecap="round" />
24
</g>
25
		<g transform="matrix(2.537 0 0 -2.537 -11.3118571171957 24.124911999999966)"  >
26
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(4.0955, -2.037)" d="m 0 0 l -2.081 -2.069 l -6.11 6.143 l 2.081 2.069 z" stroke-linecap="round" />
27
</g>
28
		<g transform="matrix(2.537 0 0 -2.537 46.27614288280432 -57.96708800000005)"  >
29
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(12.070999999999998, 9.599000000000004)" d="m 0 0 c -1.226 0.69 -2.81 0.523 -3.862 -0.524 c -1.275 -1.268 -1.28 -3.33 -0.013 -4.604 l -31.681 -31.501 l -6.11 6.143 c 19.224 19.305 25.369 35.582 25.369 35.582 c 15.857 2.364 27.851 8.624 33.821 12.335 z" stroke-linecap="round" />
30
</g>
31
		<g transform="matrix(2.537 0 0 -2.537 -26.842857117195706 8.501911999999976)"  >
32
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(4.1075, -2.0525)" d="M 0 0 L -2.081 -2.069 L -8.215 4.11 L -6.141 6.174 Z" stroke-linecap="round" />
33
</g>
34
		<g transform="matrix(2.537 0 0 -2.537 -51.495857117195726 19.491911999999985)"  >
35
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(10.434000000000001, -1.0939999999999994)" d="m 0 0 l -3.995 -3.974 l -16.873 0.96 l 14.752 9.176 z" stroke-linecap="round" />
36
</g>
37
		<g transform="matrix(2.537 0 0 -2.537 55.72014288280434 -48.441088000000036)"  >
38
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(9.671499999999998, 11.999499999999998)" d="M 0 0 L 17.536 17.443 C 13.788 11.486 7.47 -0.468 5.021 -16.312 c 0 0 -15.526 -6.982 -35.765 -25.13 l -6.135 6.168 l 31.681 31.5 c 1.273 -1.28 3.33 -1.279 4.604 -0.012 C 0.435 -2.764 0.629 -1.223 0 0" stroke-linecap="round" />
39
</g>
40
</g>
41
</g>
42
<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 643.7363123827618 766.1975713477327)" id="text-logo-path"  >
43
<path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(247,149,33); fill-rule: nonzero; opacity: 1;"  paint-order="stroke"  transform=" translate(-186.83999999999997, 27.08)" d="M 4.47 -6.1 L 4.47 -6.1 L 4.47 -47.5 Q 4.47 -50.27 6.43 -52.23 Q 8.39 -54.19 11.16 -54.19 L 11.16 -54.19 Q 14.01 -54.19 15.95 -52.23 Q 17.89 -50.27 17.89 -47.5 L 17.89 -47.5 L 17.89 -30.09 L 34.95 -51.97 Q 35.74 -52.97 36.94 -53.58 Q 38.13 -54.19 39.42 -54.19 L 39.42 -54.19 Q 41.77 -54.19 43.42 -52.5 Q 45.07 -50.82 45.07 -48.5 L 45.07 -48.5 Q 45.07 -46.46 43.82 -44.93 L 43.82 -44.93 L 32.93 -31.44 L 46.8 -9.81 Q 47.84 -8.11 47.84 -6.27 L 47.84 -6.27 Q 47.84 -3.33 45.9 -1.39 Q 43.96 0.55 41.19 0.55 L 41.19 0.55 Q 39.42 0.55 37.89 -0.29 Q 36.37 -1.14 35.43 -2.57 L 35.43 -2.57 L 23.78 -21.15 L 17.89 -13.9 L 17.89 -6.1 Q 17.89 -3.33 15.93 -1.39 Q 13.97 0.55 11.16 0.55 L 11.16 0.55 Q 8.39 0.55 6.43 -1.39 Q 4.47 -3.33 4.47 -6.1 Z M 50.27 -19.24 L 50.27 -19.24 Q 50.27 -25.13 52.71 -29.78 Q 55.16 -34.43 59.7 -37.06 Q 64.24 -39.69 70.27 -39.69 L 70.27 -39.69 Q 76.37 -39.69 80.78 -37.09 Q 85.18 -34.49 87.43 -30.32 Q 89.69 -26.14 89.69 -21.6 L 89.69 -21.6 Q 89.69 -18.69 88.33 -17.26 Q 86.98 -15.84 83.86 -15.84 L 83.86 -15.84 L 62.89 -15.84 Q 63.23 -12.38 65.38 -10.31 Q 67.53 -8.25 70.86 -8.25 L 70.86 -8.25 Q 72.84 -8.25 74.19 -8.91 Q 75.54 -9.57 76.62 -10.64 L 76.62 -10.64 Q 77.62 -11.58 78.42 -12.03 Q 79.22 -12.48 80.43 -12.48 L 80.43 -12.48 Q 82.61 -12.48 84.19 -10.89 Q 85.77 -9.29 85.77 -7.04 L 85.77 -7.04 Q 85.77 -4.54 83.62 -2.77 L 83.62 -2.77 Q 81.71 -1.14 78.16 -0.03 Q 74.61 1.07 70.58 1.07 L 70.58 1.07 Q 64.76 1.07 60.13 -1.42 Q 55.5 -3.92 52.89 -8.53 Q 50.27 -13.14 50.27 -19.24 Z M 62.96 -23.57 L 62.96 -23.57 L 76.96 -23.57 Q 76.82 -26.97 74.93 -28.97 Q 73.05 -30.96 70.06 -30.96 L 70.06 -30.96 Q 67.08 -30.96 65.21 -28.97 Q 63.34 -26.97 62.96 -23.57 Z M 91.63 -19.24 L 91.63 -19.24 Q 91.63 -25.13 94.07 -29.78 Q 96.52 -34.43 101.06 -37.06 Q 105.6 -39.69 111.63 -39.69 L 111.63 -39.69 Q 117.73 -39.69 122.14 -37.09 Q 126.54 -34.49 128.79 -30.32 Q 131.04 -26.14 131.04 -21.6 L 131.04 -21.6 Q 131.04 -18.69 129.69 -17.26 Q 128.34 -15.84 125.22 -15.84 L 125.22 -15.84 L 104.25 -15.84 Q 104.59 -12.38 106.74 -10.31 Q 108.89 -8.25 112.22 -8.25 L 112.22 -8.25 Q 114.2 -8.25 115.55 -8.91 Q 116.9 -9.57 117.98 -10.64 L 117.98 -10.64 Q 118.98 -11.58 119.78 -12.03 Q 120.58 -12.48 121.79 -12.48 L 121.79 -12.48 Q 123.97 -12.48 125.55 -10.89 Q 127.13 -9.29 127.13 -7.04 L 127.13 -7.04 Q 127.13 -4.54 124.98 -2.77 L 124.98 -2.77 Q 123.07 -1.14 119.52 -0.03 Q 115.96 1.07 111.94 1.07 L 111.94 1.07 Q 106.12 1.07 101.49 -1.42 Q 96.86 -3.92 94.24 -8.53 Q 91.63 -13.14 91.63 -19.24 Z M 104.32 -23.57 L 104.32 -23.57 L 118.32 -23.57 Q 118.18 -26.97 116.29 -28.97 Q 114.4 -30.96 111.42 -30.96 L 111.42 -30.96 Q 108.44 -30.96 106.57 -28.97 Q 104.7 -26.97 104.32 -23.57 Z M 135.03 -6.03 L 135.03 -6.03 L 135.03 -33.14 Q 135.03 -35.64 136.85 -37.46 Q 138.67 -39.28 141.13 -39.28 L 141.13 -39.28 Q 143.7 -39.28 145.52 -37.46 Q 147.34 -35.64 147.34 -33.14 L 147.34 -33.14 L 147.34 -32.17 Q 148.97 -35.36 152.09 -37.42 Q 155.21 -39.49 159.82 -39.49 L 159.82 -39.49 Q 166.93 -39.49 170.19 -35.47 Q 173.44 -31.44 173.44 -24.44 L 173.44 -24.44 L 173.44 -6.03 Q 173.44 -3.33 171.5 -1.39 Q 169.56 0.55 166.86 0.55 L 166.86 0.55 Q 164.15 0.55 162.19 -1.39 Q 160.24 -3.33 160.24 -6.03 L 160.24 -6.03 L 160.24 -22.36 Q 160.24 -26.35 158.54 -27.91 Q 156.84 -29.47 154.65 -29.47 L 154.65 -29.47 Q 152.02 -29.47 150.13 -27.58 Q 148.24 -25.69 148.24 -20.73 L 148.24 -20.73 L 148.24 -6.03 Q 148.24 -3.33 146.3 -1.39 Q 144.36 0.55 141.65 0.55 L 141.65 0.55 Q 138.95 0.55 136.99 -1.39 Q 135.03 -3.33 135.03 -6.03 Z M 177.71 -47.56 L 177.71 -47.56 Q 177.71 -50.34 179.63 -52.26 Q 181.56 -54.19 184.23 -54.19 L 184.23 -54.19 Q 186.58 -54.19 188.39 -52.73 Q 190.19 -51.27 190.71 -48.99 L 190.71 -48.99 L 197.88 -15.12 L 206.52 -48.64 Q 207.07 -51.07 209.12 -52.63 Q 211.16 -54.19 213.69 -54.19 L 213.69 -54.19 Q 216.26 -54.19 218.25 -52.57 Q 220.25 -50.96 220.8 -48.64 L 220.8 -48.64 L 229.4 -15.39 L 236.64 -49.33 Q 237.06 -51.38 238.76 -52.78 Q 240.46 -54.19 242.61 -54.19 L 242.61 -54.19 Q 245.17 -54.19 246.94 -52.4 Q 248.71 -50.62 248.71 -48.05 L 248.71 -48.05 Q 248.71 -47.56 248.57 -46.73 L 248.57 -46.73 L 239.69 -7.38 Q 238.9 -3.99 236.11 -1.72 Q 233.32 0.55 229.68 0.55 L 229.68 0.55 Q 226.14 0.55 223.37 -1.61 Q 220.59 -3.78 219.73 -7.11 L 219.73 -7.11 L 213.07 -33.45 L 206.38 -7.11 Q 205.51 -3.71 202.79 -1.58 Q 200.07 0.55 196.53 0.55 L 196.53 0.55 Q 192.89 0.55 190.17 -1.72 Q 187.45 -3.99 186.65 -7.38 L 186.65 -7.38 L 177.85 -46.14 Q 177.71 -47.15 177.71 -47.56 Z M 253.35 -6.03 L 253.35 -6.03 L 253.35 -33.14 Q 253.35 -35.64 255.17 -37.46 Q 256.99 -39.28 259.46 -39.28 L 259.46 -39.28 Q 262.02 -39.28 263.84 -37.46 Q 265.66 -35.64 265.66 -33.14 L 265.66 -33.14 L 265.66 -31.44 L 265.94 -31.44 Q 266.8 -33.56 268.1 -35.24 Q 269.4 -36.92 270.69 -37.61 L 270.69 -37.61 Q 271.9 -38.24 273.46 -38.27 L 273.46 -38.27 Q 276.65 -38.27 278.14 -36.45 Q 279.63 -34.63 279.63 -32.52 L 279.63 -32.52 Q 279.63 -30.33 278.11 -28.62 Q 276.58 -26.9 274.08 -26.9 L 274.08 -26.9 Q 272.59 -26.9 271.07 -26.26 Q 269.54 -25.62 268.47 -24.34 L 268.47 -24.34 Q 266.56 -21.98 266.56 -17.68 L 266.56 -17.68 L 266.56 -6.03 Q 266.56 -3.33 264.62 -1.39 Q 262.68 0.55 259.98 0.55 L 259.98 0.55 Q 257.27 0.55 255.31 -1.39 Q 253.35 -3.33 253.35 -6.03 Z M 282.41 -49.71 L 282.41 -49.71 Q 282.41 -52 284.03 -53.61 Q 285.66 -55.23 287.95 -55.23 L 287.95 -55.23 L 291.21 -55.23 Q 293.5 -55.23 295.13 -53.6 Q 296.76 -51.97 296.76 -49.71 L 296.76 -49.71 Q 296.76 -47.43 295.11 -45.8 Q 293.46 -44.17 291.21 -44.17 L 291.21 -44.17 L 287.95 -44.17 Q 285.66 -44.17 284.03 -45.8 Q 282.41 -47.43 282.41 -49.71 Z M 282.96 -6.03 L 282.96 -6.03 L 282.96 -32.66 Q 282.96 -35.36 284.92 -37.32 Q 286.88 -39.28 289.58 -39.28 L 289.58 -39.28 Q 292.29 -39.28 294.23 -37.32 Q 296.17 -35.36 296.17 -32.66 L 296.17 -32.66 L 296.17 -6.03 Q 296.17 -3.33 294.21 -1.39 Q 292.25 0.55 289.58 0.55 L 289.58 0.55 Q 286.88 0.55 284.92 -1.39 Q 282.96 -3.33 282.96 -6.03 Z M 299.43 -34.29 L 299.43 -34.29 Q 299.43 -36.12 300.71 -37.41 Q 301.99 -38.69 303.76 -38.69 L 303.76 -38.69 L 306.19 -38.69 L 306.46 -43.96 Q 306.6 -46.32 308.34 -47.98 Q 310.07 -49.64 312.5 -49.64 L 312.5 -49.64 Q 314.99 -49.64 316.76 -47.86 Q 318.53 -46.07 318.53 -43.58 L 318.53 -43.58 L 318.53 -38.69 L 322.72 -38.69 Q 324.49 -38.69 325.77 -37.41 Q 327.06 -36.12 327.06 -34.36 L 327.06 -34.36 Q 327.06 -32.52 325.77 -31.24 Q 324.49 -29.95 322.72 -29.95 L 322.72 -29.95 L 318.81 -29.95 L 318.81 -14.14 Q 318.81 -11.23 320.05 -10.02 Q 321.3 -8.81 323.83 -8.81 L 323.83 -8.81 Q 325.46 -8.46 326.61 -7.14 Q 327.75 -5.82 327.75 -4.06 L 327.75 -4.06 Q 327.75 -2.57 326.94 -1.39 Q 326.12 -0.21 324.84 0.35 L 324.84 0.35 Q 322 0.83 318.11 0.87 L 318.11 0.87 Q 311.28 0.9 308.44 -2.5 L 308.44 -2.5 Q 305.67 -5.79 305.67 -12.65 L 305.67 -12.65 Q 305.67 -12.83 305.67 -13 L 305.67 -13 L 305.74 -29.95 L 303.76 -29.95 Q 301.99 -29.95 300.71 -31.24 Q 299.43 -32.52 299.43 -34.29 Z M 329.8 -19.24 L 329.8 -19.24 Q 329.8 -25.13 332.24 -29.78 Q 334.68 -34.43 339.23 -37.06 Q 343.77 -39.69 349.8 -39.69 L 349.8 -39.69 Q 355.9 -39.69 360.3 -37.09 Q 364.71 -34.49 366.96 -30.32 Q 369.21 -26.14 369.21 -21.6 L 369.21 -21.6 Q 369.21 -18.69 367.86 -17.26 Q 366.51 -15.84 363.39 -15.84 L 363.39 -15.84 L 342.42 -15.84 Q 342.76 -12.38 344.91 -10.31 Q 347.06 -8.25 350.39 -8.25 L 350.39 -8.25 Q 352.37 -8.25 353.72 -8.91 Q 355.07 -9.57 356.14 -10.64 L 356.14 -10.64 Q 357.15 -11.58 357.95 -12.03 Q 358.74 -12.48 359.96 -12.48 L 359.96 -12.48 Q 362.14 -12.48 363.72 -10.89 Q 365.3 -9.29 365.3 -7.04 L 365.3 -7.04 Q 365.3 -4.54 363.15 -2.77 L 363.15 -2.77 Q 361.24 -1.14 357.69 -0.03 Q 354.13 1.07 350.11 1.07 L 350.11 1.07 Q 344.29 1.07 339.66 -1.42 Q 335.03 -3.92 332.41 -8.53 Q 329.8 -13.14 329.8 -19.24 Z M 342.48 -23.57 L 342.48 -23.57 L 356.49 -23.57 Q 356.35 -26.97 354.46 -28.97 Q 352.57 -30.96 349.59 -30.96 L 349.59 -30.96 Q 346.61 -30.96 344.74 -28.97 Q 342.87 -26.97 342.48 -23.57 Z" stroke-linecap="round" />
44
</g>
45
</svg>
A src/main/resources/com/keenwrite/logo-text.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   version="1.1"
11
   width="1280"
12
   height="1024"
13
   viewBox="0 0 1280 1024"
14
   xml:space="preserve"
15
   id="svg52"
16
   sodipodi:docname="logo-text.svg"
17
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
18
   id="metadata56"><rdf:RDF><cc:Work
19
       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
20
         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
21
   inkscape:document-rotation="0"
22
   pagecolor="#ffffff"
23
   bordercolor="#666666"
24
   borderopacity="1"
25
   objecttolerance="10"
26
   gridtolerance="10"
27
   guidetolerance="10"
28
   inkscape:pageopacity="0"
29
   inkscape:pageshadow="2"
30
   inkscape:window-width="640"
31
   inkscape:window-height="480"
32
   id="namedview54"
33
   showgrid="false"
34
   inkscape:zoom="0.78417969"
35
   inkscape:cx="642.50039"
36
   inkscape:cy="508.59942"
37
   inkscape:current-layer="svg52" />
38
<desc
39
   id="desc2">Created with Fabric.js 3.6.3</desc>
40
<defs
41
   id="defs4"><rect
42
   x="114.92139"
43
   y="132.06312"
44
   width="470.12033"
45
   height="175.55822"
46
   id="rect933" />
47
48
49
50
		
51
		
52
		
53
		
54
		
55
		
56
		
57
		
58
59
<linearGradient
60
   y2="-0.049471263"
61
   x2="0.96880889"
62
   y1="-0.044911571"
63
   x1="0.15235768"
64
   gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
65
   gradientUnits="userSpaceOnUse"
66
   id="SVGID_1_302284">
67
<stop
68
   id="stop9"
69
   style="stop-color:#ec706a;stop-opacity:1"
70
   offset="0%" />
71
<stop
72
   id="stop11"
73
   style="stop-color:#ecd980;stop-opacity:1"
74
   offset="100%" />
75
</linearGradient>
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
</defs>
92
93
<g
94
   id="g853"><path
95
     style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
96
     paint-order="stroke"
97
     d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534"
98
     stroke-linecap="round"
99
     id="path14" /><path
100
     style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
101
     paint-order="stroke"
102
     d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
103
     stroke-linecap="round"
104
     id="path22" /><path
105
     style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
106
     paint-order="stroke"
107
     d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
108
     stroke-linecap="round"
109
     id="path26" /><path
110
     style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
111
     paint-order="stroke"
112
     d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z"
113
     stroke-linecap="round"
114
     id="path30" /><path
115
     style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
116
     paint-order="stroke"
117
     d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
118
     stroke-linecap="round"
119
     id="path34" /><path
120
     style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
121
     paint-order="stroke"
122
     d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
123
     stroke-linecap="round"
124
     id="path38" /><path
125
     style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1"
126
     paint-order="stroke"
127
     d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508"
128
     stroke-linecap="round"
129
     id="path42" /></g>
130
131
<text
132
   xml:space="preserve"
133
   id="text931"
134
   style="fill:black;fill-opacity:1;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);" /><text
135
   xml:space="preserve"
136
   style="font-style:italic;font-variant:normal;font-weight:800;font-stretch:normal;font-size:133.333px;line-height:1.25;font-family:'Merriweather Sans';-inkscape-font-specification:'Merriweather Sans, Ultra-Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#51a9cf;fill-opacity:1;stroke:none;"
137
   x="311.87085"
138
   y="820.2641"
139
   id="text939"><tspan
140
     sodipodi:role="line"
141
     id="tspan937"
142
     x="311.87085"
143
     y="820.2641">KeenWrite</tspan></text></svg>
1144
A src/main/resources/com/keenwrite/logo.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
11
   sodipodi:docname="icon.svg"
12
   id="svg52"
13
   xml:space="preserve"
14
   viewBox="0 0 512 512"
15
   height="512"
16
   width="512"
17
   version="1.1"><metadata
18
   id="metadata56"><rdf:RDF><cc:Work
19
       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
20
         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
21
   inkscape:current-layer="svg52"
22
   inkscape:cy="369.17559"
23
   inkscape:cx="343.24925"
24
   inkscape:zoom="0.78417969"
25
   showgrid="false"
26
   id="namedview54"
27
   inkscape:window-height="480"
28
   inkscape:window-width="640"
29
   inkscape:pageshadow="2"
30
   inkscape:pageopacity="0"
31
   guidetolerance="10"
32
   gridtolerance="10"
33
   objecttolerance="10"
34
   borderopacity="1"
35
   bordercolor="#666666"
36
   pagecolor="#ffffff"
37
   inkscape:document-rotation="0" />
38
<desc
39
   id="desc2">Created with Fabric.js 3.6.3</desc>
40
<defs
41
   id="defs4"><rect
42
   id="rect933"
43
   height="175.55823"
44
   width="470.12033"
45
   y="132.06313"
46
   x="114.92139" />
47
48
49
50
		
51
		
52
		
53
		
54
		
55
		
56
		
57
		
58
59
<linearGradient
60
   id="SVGID_1_302284"
61
   gradientUnits="userSpaceOnUse"
62
   gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
63
   x1="0.15235768"
64
   y1="-0.044911571"
65
   x2="0.96880889"
66
   y2="-0.049471263">
67
<stop
68
   offset="0%"
69
   style="stop-color:#ec706a;stop-opacity:1"
70
   id="stop9" />
71
<stop
72
   offset="100%"
73
   style="stop-color:#ecd980;stop-opacity:1"
74
   id="stop11" />
75
</linearGradient>
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
</defs>
92
93
<g
94
   transform="translate(-384.01706,-164.40168)"
95
   id="g853"><path
96
     id="path14"
97
     stroke-linecap="round"
98
     d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534"
99
     paint-order="stroke"
100
     style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
101
     id="path22"
102
     stroke-linecap="round"
103
     d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
104
     paint-order="stroke"
105
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
106
     id="path26"
107
     stroke-linecap="round"
108
     d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
109
     paint-order="stroke"
110
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
111
     id="path30"
112
     stroke-linecap="round"
113
     d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z"
114
     paint-order="stroke"
115
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
116
     id="path34"
117
     stroke-linecap="round"
118
     d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
119
     paint-order="stroke"
120
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
121
     id="path38"
122
     stroke-linecap="round"
123
     d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
124
     paint-order="stroke"
125
     style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path
126
     id="path42"
127
     stroke-linecap="round"
128
     d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508"
129
     paint-order="stroke"
130
     style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /></g>
131
132
<text
133
   style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);fill:#000000;fill-opacity:1;stroke:none;"
134
   id="text931"
135
   xml:space="preserve" /></svg>
1136
A src/main/resources/com/keenwrite/logo128.png
Binary file
A src/main/resources/com/keenwrite/logo16.png
Binary file
A src/main/resources/com/keenwrite/logo256.png
Binary file
A src/main/resources/com/keenwrite/logo32.png
Binary file
A src/main/resources/com/keenwrite/logo512.png
Binary file
A src/main/resources/com/keenwrite/logo64.png
Binary file
A src/main/resources/com/keenwrite/messages.properties
1
# ########################################################################
2
# Main Application Window
3
# ########################################################################
4
5
# suppress inspection "UnusedProperty" for whole file
6
7
Main.menu.file=_File
8
Main.menu.file.new=_New
9
Main.menu.file.open=_Open...
10
Main.menu.file.close=_Close
11
Main.menu.file.close_all=Close All
12
Main.menu.file.save=_Save
13
Main.menu.file.save_as=Save _As
14
Main.menu.file.save_all=Save A_ll
15
Main.menu.file.exit=E_xit
16
17
Main.menu.edit=_Edit
18
Main.menu.edit.copy.html=Copy _HTML
19
Main.menu.edit.undo=_Undo
20
Main.menu.edit.redo=_Redo
21
Main.menu.edit.cut=Cu_t
22
Main.menu.edit.copy=_Copy
23
Main.menu.edit.paste=_Paste
24
Main.menu.edit.selectAll=Select _All
25
Main.menu.edit.find=_Find
26
Main.menu.edit.find.next=Find _Next
27
Main.menu.edit.preferences=_Preferences
28
29
Main.menu.insert=_Insert
30
Main.menu.insert.blockquote=_Blockquote
31
Main.menu.insert.code=Inline _Code
32
Main.menu.insert.fenced_code_block=_Fenced Code Block
33
Main.menu.insert.fenced_code_block.prompt=Enter code here
34
Main.menu.insert.link=_Link...
35
Main.menu.insert.image=_Image...
36
Main.menu.insert.heading.1=Heading _1
37
Main.menu.insert.heading.1.prompt=heading 1
38
Main.menu.insert.heading.2=Heading _2
39
Main.menu.insert.heading.2.prompt=heading 2
40
Main.menu.insert.heading.3=Heading _3
41
Main.menu.insert.heading.3.prompt=heading 3
42
Main.menu.insert.unordered_list=_Unordered List
43
Main.menu.insert.ordered_list=_Ordered List
44
Main.menu.insert.horizontal_rule=_Horizontal Rule
45
46
Main.menu.format=Forma_t
47
Main.menu.format.bold=_Bold
48
Main.menu.format.italic=_Italic
49
Main.menu.format.superscript=Su_perscript
50
Main.menu.format.subscript=Su_bscript
51
Main.menu.format.strikethrough=Stri_kethrough
52
53
Main.menu.definition=_Definition
54
Main.menu.definition.create=_Create
55
Main.menu.definition.insert=_Insert
56
57
Main.menu.help=_Help
58
Main.menu.help.about=About
59
60
# ########################################################################
61
# Status Bar
62
# ########################################################################
63
64
Main.status.text.offset=offset
65
Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
66
Main.status.state.default=OK
67
Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
68
Main.status.error.def.blank=Move the caret to a word before inserting a definition.
69
Main.status.error.def.empty=Create a definition before inserting a definition.
70
Main.status.error.def.missing=No definition value found for ''{0}''.
71
Main.status.error.r=Error with [{0}...]: {1}
72
Main.status.error.file.missing=Not found: {0}
73
74
# ########################################################################
75
# Preferences
76
# ########################################################################
77
78
Preferences.r=R
79
Preferences.r.script=Startup Script
80
Preferences.r.script.desc=Script runs prior to executing R statements within the document.
81
Preferences.r.directory=Working Directory
82
Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script.
83
Preferences.r.delimiter.began=Delimiter Prefix
84
Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions.
85
Preferences.r.delimiter.ended=Delimiter Suffix
86
Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions.
87
88
Preferences.images=Images
89
Preferences.images.directory=Relative Directory
90
Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths.
91
Preferences.images.suffixes=Extensions
92
Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces.
93
94
Preferences.definitions=Definitions
95
Preferences.definitions.path=File name
96
Preferences.definitions.path.desc=Absolute path to interpolated string definitions.
97
Preferences.definitions.delimiter.began=Delimiter Prefix
98
Preferences.definitions.delimiter.began.desc=Indicates when a definition key is starting.
99
Preferences.definitions.delimiter.ended=Delimiter Suffix
100
Preferences.definitions.delimiter.ended.desc=Indicates when a definition key is ending.
101
102
Preferences.fonts=Editor
103
Preferences.fonts.size_editor=Font Size
104
Preferences.fonts.size_editor.desc=Font size to use for the text editor.
105
106
# ########################################################################
107
# Definition Pane and its Tree View
108
# ########################################################################
109
110
Definition.menu.create=Create
111
Definition.menu.rename=Rename
112
Definition.menu.remove=Delete
113
Definition.menu.add.default=Undefined
114
115
# ########################################################################
116
# Failure messages with respect to YAML files.
117
# ########################################################################
118
yaml.error.open=Could not open YAML file (ensure non-empty file).
119
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
120
yaml.error.missing=Empty definition value for key ''{0}''.
121
yaml.error.tree.form=Unassigned definition near ''{0}''.
122
123
# ########################################################################
124
# File Editor
125
# ########################################################################
126
127
FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
128
FileEditor.loadFailed.title=Load
129
FileEditor.loadFailed.reason.permissions=File must be readable and writable.
130
FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
131
FileEditor.saveFailed.title=Save
132
133
# ########################################################################
134
# File Open
135
# ########################################################################
136
137
Dialog.file.choose.open.title=Open File
138
Dialog.file.choose.save.title=Save File
139
140
Dialog.file.choose.filter.title.source=Source Files
141
Dialog.file.choose.filter.title.definition=Definition Files
142
Dialog.file.choose.filter.title.xml=XML Files
143
Dialog.file.choose.filter.title.all=All Files
144
145
# ########################################################################
146
# Alert Dialog
147
# ########################################################################
148
149
Alert.file.close.title=Close
150
Alert.file.close.text=Save changes to {0}?
151
152
# ########################################################################
153
# Definition Pane
154
# ########################################################################
155
156
Pane.definition.node.root.title=Definitions
157
Pane.definition.button.create.label=_Create
158
Pane.definition.button.rename.label=_Rename
159
Pane.definition.button.delete.label=_Delete
160
Pane.definition.button.create.tooltip=Add new item (Insert)
161
Pane.definition.button.rename.tooltip=Rename selected item (F2)
162
Pane.definition.button.delete.tooltip=Delete selected items (Delete)
163
164
# Controls ###############################################################
165
166
# ########################################################################
167
# Browse File
168
# ########################################################################
169
170
BrowseFileButton.chooser.title=Browse for local file
171
BrowseFileButton.chooser.allFilesFilter=All Files
172
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
173
174
# Dialogs ################################################################
175
176
# ########################################################################
177
# Image
178
# ########################################################################
179
180
Dialog.image.title=Image
181
Dialog.image.chooser.imagesFilter=Images
182
Dialog.image.previewLabel.text=Markdown Preview\:
183
Dialog.image.textLabel.text=Alternate Text\:
184
Dialog.image.titleLabel.text=Title (tooltip)\:
185
Dialog.image.urlLabel.text=Image URL\:
186
187
# ########################################################################
188
# Hyperlink
189
# ########################################################################
190
191
Dialog.link.title=Link
192
Dialog.link.previewLabel.text=Markdown Preview\:
193
Dialog.link.textLabel.text=Link Text\:
194
Dialog.link.titleLabel.text=Title (tooltip)\:
195
Dialog.link.urlLabel.text=Link URL\:
196
197
# ########################################################################
198
# About
199
# ########################################################################
200
201
Dialog.about.title=About {0}
202
Dialog.about.header={0}
203
Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
1204
A src/main/resources/com/keenwrite/preview/webview.css
1
/* RESET ***/
2
html{box-sizing:border-box;font-size:12pt}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0}
3
4
/* BODY ***/
5
body {
6
  /* Must be bundled in JAR file. */
7
  font-family: "Vollkorn", serif;
8
  background-color: #fff;
9
  margin: 0 auto;
10
  max-width: 960px;
11
  line-height: 1.6;
12
  color: #454545;
13
  padding: 1em;
14
  font-feature-settings: "liga" 1;
15
  font-variant-ligatures: normal;
16
}
17
18
body>*:first-child {
19
  margin-top: 0 !important;
20
}
21
22
body>*:last-child {
23
  margin-bottom: 0 !important;
24
}
25
26
/* BLOCKS ***/
27
p, blockquote, ul, ol, dl, table, pre {
28
  margin: 1em 0;
29
}
30
31
/* HEADINGS ***/
32
h1, h2, h3, h4, h5, h6 {
33
  font-weight: bold;
34
  margin: 1em 0 .5em;
35
}
36
37
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
38
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
39
  font-size: inherit;
40
}
41
42
h1 {
43
  font-size: 21pt;
44
}
45
46
h2 {
47
  font-size: 18pt;
48
  border-bottom: 1px solid #ccc;
49
}
50
51
h3 {
52
  font-size: 15pt;
53
}
54
55
h4 {
56
  font-size: 13.5pt;
57
}
58
59
h5 {
60
  font-size: 12pt;
61
}
62
63
h6 {
64
  font-size: 10.5pt;
65
}
66
67
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
68
  margin-top: .5em;
69
}
70
71
/* LINKS ***/
72
a {
73
  color: #0077aa;
74
  text-decoration: none;
75
}
76
77
a:hover {
78
  text-decoration: underline;
79
}
80
81
/* BULLET LISTS ***/
82
ul, ol {
83
  display: block;
84
  list-style: disc outside none;
85
  margin: 1em 0;
86
  padding: 0 0 0 2em;
87
}
88
89
ol {
90
  list-style-type: decimal;
91
}
92
93
ul ul, ol ul,
94
ol ol, ul ol {
95
  list-style-position: inside;
96
  margin-left: 1em;
97
}
98
99
ul ul, ol ul {
100
  list-style-type: circle;
101
}
102
103
ol ol, ul ol {
104
  list-style-type: lower-latin;
105
}
106
107
/* DEFINITION LISTS ***/
108
dl {
109
  /** Horizontal scroll bar will appear if set to 100%. */
110
  width: 99%;
111
  overflow: hidden;
112
  padding-left: 1em;
113
}
114
115
dl dt {
116
  font-weight: bold;
117
  float: left;
118
  width: 20%;
119
  clear: both;
120
  position: relative;
121
}
122
123
dl dd {
124
  float: right;
125
  width: 79%;
126
  padding-bottom: .5em;
127
  margin-left: 0;
128
}
129
130
/* CODE ***/
131
pre, code, tt {
132
  /* Must be bundled in JAR file. */
133
  font-family: "Fira Code", monospace;
134
  font-size: 10pt;
135
  background-color: #f8f8f8;
136
  text-decoration: none;
137
  white-space: pre-wrap;
138
  word-wrap: break-word;
139
  overflow-wrap: anywhere;
140
  border-radius: .125em;
141
}
142
143
code, tt {
144
  padding: .25em;
145
}
146
147
pre > code {
148
  /* Reset the padding. */
149
  padding: 0;
150
  border: none;
151
  background: transparent;
152
}
153
154
pre {
155
  border: .125em solid #ccc;
156
  overflow: auto;
157
  /* Assign the new padding, independently from previous. */
158
  padding: .25em .5em;
159
}
160
161
pre code, pre tt {
162
  background-color: transparent;
163
  border: none;
164
}
165
166
/* QUOTES ***/
167
blockquote {
168
  border-left: .25em solid #ccc;
169
  padding: 0 1em;
170
  color: #777;
171
}
172
173
blockquote>:first-child {
174
  margin-top: 0;
175
}
176
177
blockquote>:last-child {
178
  margin-bottom: 0;
179
}
180
181
/* HORIZONTAL RULES ***/
182
hr {
183
  clear: both;
184
  margin: 1.5em 0 1.5em;
185
  height: 0;
186
  overflow: hidden;
187
  border: none;
188
  background: transparent;
189
  border-bottom: .125em solid #ccc;
190
}
191
192
/* TABLES ***/
193
table {
194
  width: 100%;
195
}
196
197
tr:nth-child(odd) {
198
  background-color: #eee;
199
}
200
201
th {
202
  background-color: #454545;
203
  color: #fff;
204
}
205
206
th, td {
207
  text-align: left;
208
  padding: 0 1em;
209
}
210
211
/* IMAGES ***/
212
img {
213
  max-width: 100%;
214
}
215
216
/* Required for FlyingSaucer to detect the node.
217
 * See SVGReplacedElementFactory for details.
218
 */
219
tex {
220
  /* Ensure the formulas can be inlined with text. */
221
  display: inline-block;
222
}
223
224
/* Without a robust typesetting engine, there's no
225
 * nice-looking way to automatically typeset equations.
226
 * Sometimes baseline is appropriate, sometimes the
227
 * descender must be considered, and sometimes vertical
228
 * alignment to the middle looks best.
229
 */
230
p tex {
231
  vertical-align: baseline;
232
}
1233
A src/main/resources/com/keenwrite/scene.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
/*---- toolbar ----*/
29
30
.tool-bar {
31
	-fx-spacing: 0;
32
}
33
34
.tool-bar .button {
35
	-fx-background-color: transparent;
36
}
37
38
.tool-bar .button:hover {
39
	-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
40
	-fx-color: -fx-hover-base;
41
}
42
43
.tool-bar .button:armed {
44
	-fx-color: -fx-pressed-base;
45
}
146
A src/main/resources/com/keenwrite/settings.properties
1
# ########################################################################
2
# Application
3
# ########################################################################
4
5
application.title=keenwrite
6
application.package=com/${application.title}
7
application.messages= com.${application.title}.messages
8
9
# Suppress multiple file modified notifications for one logical modification.
10
# Given in milliseconds.
11
application.watchdog.timeout=50
12
13
# ########################################################################
14
# Preferences
15
# ########################################################################
16
17
preferences.root=com.${application.title}
18
preferences.root.state=state
19
preferences.root.options=options
20
preferences.root.definition.source=definition.source
21
22
# ########################################################################
23
# File and Path References
24
# ########################################################################
25
file.stylesheet.scene=${application.package}/scene.css
26
file.stylesheet.markdown=${application.package}/editor/markdown.css
27
file.stylesheet.preview=webview.css
28
file.stylesheet.xml=${application.package}/xml.css
29
30
file.logo.16 =${application.package}/logo16.png
31
file.logo.32 =${application.package}/logo32.png
32
file.logo.128=${application.package}/logo128.png
33
file.logo.256=${application.package}/logo256.png
34
file.logo.512=${application.package}/logo512.png
35
36
# Default file name when a new file is created.
37
# This ensures that the file type can always be
38
# discerned so that the correct type of variable
39
# reference can be inserted.
40
file.default=untitled.md
41
file.definition.default=variables.yaml
42
43
# ########################################################################
44
# File name Extensions
45
# ########################################################################
46
47
# Comma-separated list of definition file name extensions.
48
definition.file.ext.json=*.json
49
definition.file.ext.toml=*.toml
50
definition.file.ext.yaml=*.yml,*.yaml
51
definition.file.ext.properties=*.properties,*.props
52
53
# Comma-separated list of file name extensions.
54
file.ext.rmarkdown=*.Rmd
55
file.ext.rxml=*.Rxml
56
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
57
file.ext.definition=${definition.file.ext.yaml}
58
file.ext.xml=*.xml,${file.ext.rxml}
59
file.ext.all=*.*
60
61
# File name extension search order for images.
62
file.ext.image.order=svg pdf png jpg tiff
63
64
# ########################################################################
65
# Variable Name Editor
66
# ########################################################################
67
68
# Maximum number of characters for a variable name. A variable is defined
69
# as one or more non-whitespace characters up to this maximum length.
70
editor.variable.maxLength=256
71
72
# ########################################################################
73
# Dialog Preferences
74
# ########################################################################
75
76
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
77
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
78
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
79
80
# Ensures a consistent button order for alert dialogs across platforms (because
81
# the default button order on Linux defies all logic).
82
dialog.alert.button.order=${dialog.alert.button.order.windows}
183
A src/main/resources/com/keenwrite/variables.yaml
1
---
2
c:
3
  protagonist:
4
    name:
5
      First: Chloe
6
      First_pos: $c.protagonist.name.First$'s
7
      Middle: Irene
8
      Family: Angelos
9
      nick:
10
        Father: Savant
11
        Mother: Sweetie
12
    colour:
13
      eyes: green
14
      hair: dark auburn
15
      syn_1: black
16
      syn_2: purple
17
      syn_11: teal
18
      syn_6: silver
19
      favourite: emerald green
20
    speech:
21
      tic: oh
22
    father:
23
      heritage: Greek
24
      name:
25
        Short: Bryce
26
        First: Bryson
27
        First_pos: $c.protagonist.father.name.First$'s
28
        Honourific: Mr.
29
      education: Masters
30
      vocation:
31
        name: robotics
32
        title: roboticist
33
      employer:
34
        name:
35
          Short: Rabota
36
          Full: $c.protagonist.father.employer.name.Short$ Designs
37
      hair:
38
        style: thick, curly
39
        colour: black
40
      eyes:
41
        colour: dark brown
42
      Endear: Dad
43
      vehicle: coupé
44
    mother:
45
      name:
46
        Short: Cass
47
        First: Cassandra
48
        First_pos: $c.protagonist.mother.name.First$'s
49
        Honourific: Mrs.
50
      education: PhD
51
      speech:
52
        tic: cute
53
        Honorific: Doctor
54
      vocation:
55
        article: an
56
        name: oceanography
57
        title: oceanographer
58
      employer:
59
        name:
60
          Full: Oregon State University
61
          Short: OSU
62
      eyes:
63
        colour: blue
64
      hair:
65
        style: thick, curly
66
        colour: dark brown
67
      Endear: Mom
68
      Endear_pos: Mom's
69
    uncle:
70
      name:
71
        First: Damian
72
        First_pos: $c.protagonist.uncle.name.First$'s
73
        Family: Moros
74
      hands:
75
        fingers:
76
          shape: long, bony
77
    friend:
78
      primary:
79
        name:
80
          First: Gerard
81
          First_pos: $c.protagonist.friend.primary.name.First$'s
82
          Family: Baran
83
          Family_pos: $c.protagonist.friend.primary.name.Family$'s
84
        favourite:
85
          colour: midnight blue
86
        eyes:
87
          colour: hazel
88
        mother:
89
          name:
90
            First: Isabella
91
            Short: Izzy
92
            Honourific: Mrs.
93
        father:
94
          name:
95
            Short: Mo
96
            First: Montgomery
97
            First_pos: $c.protagonist.friend.primary.father.name.First$'s
98
            Honourific: Mr.
99
          speech:
100
            tic: y'know
101
          endear: Pops
102
  military:
103
    primary:
104
      name:
105
        First: Felix
106
        Family: LeMay
107
        Family_pos: LeMay's
108
      rank:
109
        Short: General
110
        Full: Brigadier $c.military.primary.rank.Short$
111
      colour:
112
        eyes: gray
113
        hair: dirty brown
114
    secondary:
115
      name:
116
        Family: Grell
117
      rank: Colonel
118
      colour:
119
        eyes: green
120
        hair: deep red
121
    quaternary:
122
      name:
123
        First: Gretchen
124
        Family: Steinherz
125
  minor:
126
    primary:
127
      name:
128
        First: River
129
        Family: Banks
130
        Honourific: Mx.
131
      vocation:
132
        title: salesperson
133
      employer:
134
        Name: Geophysical Prospecting Incorporated
135
        Abbr: GPI
136
        Area: Cold Spring Creek
137
        payment: twenty million
138
    secondary:
139
      name:
140
        First: Renato
141
        Middle: Carroña
142
        Family: Salvatierra
143
        Family_pos: $c.minor.secondary.name.Family$'s
144
        Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$
145
        Honourific: Mister
146
        Honourific_sp: Señor
147
      vocation:
148
        title: detective
149
    tertiary:
150
      name:
151
        First: Robert
152
        Family: Hanssen
153
154
  ai:
155
    protagonist:
156
      name:
157
        first: yoky
158
        First: Yoky
159
        First_pos: $c.ai.protagonist.name.First$'s
160
        Family: Tsukuda
161
        id: 46692
162
      persona:
163
        name:
164
          First: Hoshi
165
          First_pos: $c.ai.protagonist.persona.name.First$'s
166
          Family: Yamamoto
167
          Family_pos: $c.ai.protagonist.persona.name.Family$'s
168
      culture: Japanese-American
169
      ethnicity: Asian
170
      rank: Technical Sergeant
171
      speech:
172
        tic: okay
173
    first:
174
      Name: Prôtos
175
      Name_pos: Prôtos'
176
      age:
177
        actual: twenty-six weeks
178
        virtual: five years
179
    second:
180
      Name: Défteros
181
    third:
182
      Name: Trítos
183
    fourth:
184
      Name: Tétartos
185
    material:
186
      type: metal
187
      raw: ilmenite
188
      extract: ore
189
      name:
190
        short: titanium
191
        long: $c.ai.material.name.short$ dioxide
192
        Abbr: TiO~2~
193
      pejorative: tin
194
  animal:
195
    protagonist:
196
      Name: Trufflers
197
      type: pig
198
    antagonist:
199
      name: coywolf
200
      Name: Coywolf
201
      plural: coywolves
202
203
narrator:
204
  one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$)
205
  two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$)
206
207
military:
208
  name:
209
    Short: Agency
210
    Short_pos: $military.name.Short$'s
211
    plural: agencies
212
  machine:
213
    Name: Skopós
214
    Name_pos: $military.machine.Name$'
215
    Location: Arctic
216
    predictor: quantum chips
217
  land:
218
    name:
219
      Full: $military.name.Short$ of Defence
220
    Slogan: Safety in Numbers
221
  air:
222
    name:
223
      Full: $military.name.Short$ of Air
224
  compound:
225
    type: base
226
    lights:
227
      colour: blue
228
    nick:
229
      Prefix: Catacombs
230
      prep: of
231
      Suffix: Tartarus
232
233
government:
234
  Country: United States
235
236
location:
237
  protagonist:
238
    City: Corvallis
239
    Region: Oregon
240
    Geography: Willamette Valley
241
    secondary:
242
      City: Willow Branch Spring
243
      Region: Oregon
244
      Geography: Wheeler County
245
      Water: Clarno Rapids
246
      Road: Shaniko-Fossil Highway
247
    tertiary:
248
      City: Leavenworth
249
      Region: Washington
250
      Type: Bavarian village
251
    school:
252
      address: 1400 Northwest Buchanan Avenue
253
    hospital:
254
      Name: Good Samaritan Regional Medical Center
255
  ai:
256
    escape:
257
      country:
258
        Name: Ecuador
259
        Name_pos: Ecuador's
260
      mountain:
261
        Name: Chimborazo
262
263
language:
264
  ai:
265
    article: an
266
    singular: exanimis
267
    plural: exanimēs
268
    brain:
269
      singular: superum
270
      plural: supera
271
    title: memristor array
272
    Title: Memristor Array
273
  police:
274
    slang:
275
      singular: mippo
276
      plural: $language.police.slang.singular$s
277
278
date:
279
  anchor: 2042-09-02
280
  protagonist:
281
    born: 0
282
    conceived: -243
283
    attacked:
284
      first: 2192
285
      second: 8064
286
    father:
287
      attacked:
288
        first: -8205
289
      date:
290
        second: -1550
291
    family:
292
      moved:
293
        first: $date.protagonist.conceived$ + 35
294
  game:
295
    played:
296
      first: $date.protagonist.born$ - 672
297
      second: $date.protagonist.family.moved.first$ + 2
298
  ai:
299
    interviewed: 6198
300
    onboarded: $date.ai.interviewed$ + 290
301
    diagnosed: $date.ai.onboarded$ + 2
302
    resigned: $date.ai.diagnosed$ + 3
303
    trapped: $date.ai.resigned$ + 26
304
    torturer: $date.ai.trapped$ + 18
305
    memristor: $date.ai.torturer$ + 61
306
    ethics: $date.ai.memristor$ + 415
307
    trained: $date.ai.ethics$ + 385
308
    mindjacked: $date.ai.trained$ + 22
309
    bombed: $date.ai.mindjacked$ + 458
310
  military:
311
    machine:
312
      Construction: Six years
313
314
plot:
315
  Log: $c.ai.protagonist.name.First_pos$ Chronicles
316
  Channel: Quantum Channel
317
318
  device:
319
    computer:
320
      Name: Tau
321
    network:
322
      Name: Internet
323
    paper:
324
      name:
325
        full: electronic sheet
326
        short: sheet
327
    typewriter:
328
      Name: Underwood
329
      year: nineteen twenties
330
      room: root cellar
331
    portable:
332
      name: nanobook
333
    vehicle:
334
      name: robocars
335
      Name: Robocars
336
    sensor:
337
      name: BMP1580
338
    phone:
339
      name: comm
340
      name_pos: $plot.device.phone.name$'s
341
      Name: Comm
342
      plural: $plot.device.phone.name$s
343
    video:
344
      name: vidfeed
345
      plural: $plot.device.video.name$s
346
    game:
347
      Name: Psynæris
348
      thought: transed
349
      machine: telecognos
350
      location:
351
        Building: Nijō Castle
352
        District: Gion
353
        City: Kyoto
354
        Country: Japan
355
356
farm:
357
  population:
358
    estimate: 350
359
    actual: 1,000
360
  energy: 9800kJ
361
  width: 55m
362
  length: 55m
363
  storeys: 10
364
365
lamp:
366
  height: 0.17m
367
  length: 1.22m
368
  width: 0.28m
369
370
crop:
371
  name: 
372
    singular: tomato
373
    plural: $crop.name.singular$es
374
  energy: 318kJ
375
  weight: 450g
376
  yield: 50
377
  harvests: 7
378
  diameter: 2m
379
  height: 1.5m
380
381
heading:
382
  ch_01: Till
383
  ch_02: Sow
384
  ch_03: Seed
385
  ch_04: Germinate
386
  ch_05: Grow
387
  ch_06: Shoot
388
  ch_07: Bud
389
  ch_08: Bloom
390
  ch_09: Pollinate
391
  ch_10: Fruit
392
  ch_11: Harvest
393
  ch_12: Deliver
394
  ch_13: Spoil
395
  ch_14: Revolt
396
  ch_15: Compost
397
  ch_16: Burn
398
  ch_17: Release
399
  ch_18: End Notes
400
  ch_19: Characters
401
402
inference:
403
  unit: per cent
404
  min: two
405
  ch_sow: eighty
406
  ch_seed: fifty-two
407
  ch_germinate: thirty-one
408
  ch_grow: fifteen
409
  ch_shoot: seven
410
  ch_bloom: four
411
  ch_pollinate: two
412
  ch_harvest: ninety-five
413
  ch_delivery: ninety-eight
414
415
link:
416
  tartarus: https://en.wikipedia.org/wiki/Tartarus
417
  exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls
418
  atalanta: https://en.wikipedia.org/wiki/Atalanta
419
  detain: https://goo.gl/RCNuOQ
420
  ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics
421
  algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon
422
  holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust
423
  memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf
424
  surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487
425
  tor: https://www.torproject.org
426
  hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra
427
  foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134
428
  drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist
429
  fermi: https://arxiv.org/pdf/1404.0204v1.pdf
430
  face: https://www.youtube.com/watch?v=ladqJQLR2bA
431
  expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures
432
  governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531
433
  asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
434
  clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws
435
  jetpack: http://jetpackaviation.com/
436
  hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ
437
  eyes_five: https://en.wikipedia.org/wiki/Five_Eyes
438
  eyes_nine: https://www.privacytools.io/
439
  eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html
440
  tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml
441
1442
A src/main/resources/com/keenwrite/xml.css
1
.tagmark {
2
    -fx-fill: gray;
3
}
4
.anytag {
5
    -fx-fill: crimson;
6
}
7
.paren {
8
    -fx-fill: firebrick;
9
    -fx-font-weight: bold;
10
}
11
.attribute {
12
    -fx-fill: darkviolet;
13
}
14
.avalue {
15
    -fx-fill: black;
16
}
117
18
.comment {
19
	-fx-fill: teal;
20
}
D src/main/resources/com/scrivenvar/.gitignore
1
app.properties
21
D src/main/resources/com/scrivenvar/build.sh
1
#!/bin/bash
2
3
INKSCAPE="/usr/bin/inkscape"
4
PNG_COMPRESS="optipng"
5
PNG_COMPRESS_OPTS="-o9 *png"
6
ICO_TOOL="icotool"
7
ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
8
9
declare -a SIZES=("16" "32" "64" "128" "256" "512")
10
11
for i in "${SIZES[@]}"; do
12
  # -y: export background opacity 0
13
  $INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png"
14
done
15
16
# Compess the PNG images.
17
which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
18
19
# Generate an ICO file.
20
which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
21
221
D src/main/resources/com/scrivenvar/editor/markdown.css
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
29
.markdown-editor {
30
  -fx-font-size: 11pt;
31
}
32
33
/* Subtly highlight the current paragraph. */
34
.markdown-editor .paragraph-box:has-caret {
35
  -fx-background-color: #fcfeff;
36
}
37
38
/* Light colour for selection highlight. */
39
.markdown-editor .selection {
40
  -fx-fill: #a6d2ff;
41
}
42
43
/* Decoration for words not found in the lexicon. */
44
.markdown-editor .spelling {
45
  -rtfx-underline-color: rgba(255, 131, 67, .7);
46
  -rtfx-underline-dash-array: 4, 2;
47
  -rtfx-underline-width: 2;
48
  -rtfx-underline-cap: round;
49
}
501
D src/main/resources/com/scrivenvar/logo.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4
<svg
5
   xmlns:dc="http://purl.org/dc/elements/1.1/"
6
   xmlns:cc="http://creativecommons.org/ns#"
7
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
   xmlns:svg="http://www.w3.org/2000/svg"
9
   xmlns="http://www.w3.org/2000/svg"
10
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
   id="svg2"
13
   version="1.1"
14
   inkscape:version="0.91 r13725"
15
   width="512"
16
   height="512"
17
   viewBox="0 0 512 512"
18
   sodipodi:docname="logo.svg">
19
  <metadata
20
     id="metadata8">
21
    <rdf:RDF>
22
      <cc:Work
23
         rdf:about="">
24
        <dc:format>image/svg+xml</dc:format>
25
        <dc:type
26
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
27
        <dc:title></dc:title>
28
      </cc:Work>
29
    </rdf:RDF>
30
  </metadata>
31
  <defs
32
     id="defs6" />
33
  <sodipodi:namedview
34
     pagecolor="#ffffff"
35
     bordercolor="#666666"
36
     borderopacity="1"
37
     objecttolerance="10"
38
     gridtolerance="10"
39
     guidetolerance="10"
40
     inkscape:pageopacity="0"
41
     inkscape:pageshadow="2"
42
     inkscape:window-width="640"
43
     inkscape:window-height="480"
44
     id="namedview4"
45
     showgrid="false"
46
     fit-margin-top="0"
47
     fit-margin-left="0"
48
     fit-margin-right="0"
49
     fit-margin-bottom="0"
50
     inkscape:zoom="1.2682274"
51
     inkscape:cx="15.646213"
52
     inkscape:cy="213.34955"
53
     inkscape:current-layer="svg2" />
54
  <path
55
     style="fill:#ce6200;fill-opacity:1"
56
     d="m 203.2244,511.85078 c -60.01827,-1.2968 -121.688643,-6.5314 -192.436493,-16.334 -5.8078027,-0.8047 -10.66110747,-1.561 -10.78511762,-1.6806 -0.12404567,-0.1196 3.90488112,-4.5812 8.95313512,-9.9147 32.9484785,-34.8102 70.4314485,-73.8923 104.1521555,-108.5956 l 11.87611,-12.2221 5.48905,-10.2177 c 35.82801,-66.6927 75.13064,-128.5665 105.90637,-166.7277 6.13805,-7.611 10.21451,-12.0689 17.28719,-18.9048 36.6818,-35.4537 108.27279,-83.724003 206.0323,-138.917303 22.10365,-12.47935 51.93386,-28.64995037 52.26391,-28.33165037 0.38883,0.37499 -2.35932,25.95575037 -4.86585,45.29275037 -7.28943,56.236403 -17.04619,103.128903 -28.07642,134.939803 -7.19617,20.7536 -14.81287,35.152 -22.9667,43.4155 -3.60444,3.6529 -6.58328,5.7941 -10.1313,7.2825 l -2.56414,1.0756 -53.43164,0.1713 -53.43166,0.1713 3.69973,1.8547 c 26.78565,13.4282 52.58051,27.5241 59.57122,32.5533 4.48397,3.2259 4.41278,2.9854 1.59124,5.3784 -26.99514,22.8955 -74.52961,44.0013 -140.23089,62.2641 -26.34995,7.3244 -57.85469,14.6842 -86.99871,20.3237 l -10.26943,1.9871 -52.01052,53.2733 -52.010524,53.2732 -29.459801,15.1165 c -26.4100885,13.5517 -29.3446639,15.1388 -28.347645,15.3311 0.6117029,0.118 4.0894221,0.2188 7.7282726,0.2239 3.6388854,0.01 16.1273694,0.2329 27.7522124,0.5059 51.576376,1.2116 146.083985,1.512 170.154295,0.5409 34.66996,-1.3988 52.7606,-2.9325 67.58258,-5.7293 2.68664,-0.507 4.82907,-0.9755 4.76094,-1.0412 -0.0681,-0.066 -3.24733,-0.8833 -7.0649,-1.8169 -8.04133,-1.9664 -25.10167,-5.3107 -41.1231,-8.0612 -47.6405,-8.1787 -65.48708,-12.0107 -74.13028,-15.9169 -3.90548,-1.7651 -7.13816,-4.7659 -8.12937,-7.5463 -1.01822,-2.8562 -0.92214,-6.5271 0.23315,-8.9083 1.86563,-3.8451 6.14837,-6.7199 12.26745,-8.2345 16.96993,-4.2004 57.27977,-6.1832 90.36228,-4.4448 54.7332,2.8761 117.0767,13.1228 178.50212,29.3385 18.03514,4.7611 51.66065,14.656 51.22677,15.0744 -0.0824,0.08 -5.72762,-0.854 -12.54488,-2.0745 -40.1043,-7.18 -60.50854,-10.2888 -101.40822,-15.4507 -24.4851,-3.0902 -55.12614,-5.9915 -77.58876,-7.3465 -26.58826,-1.6039 -61.15821,-1.7754 -80.99202,-0.4019 l -3.19705,0.2214 8.70308,1.4934 c 51.89698,8.9047 77.51746,14.9877 88.00479,20.8948 6.9134,3.894 10.30497,9.4381 9.33333,15.2569 -1.50397,9.0066 -10.51381,14.0257 -32.00273,17.8278 -16.31374,2.8863 -47.27575,4.3845 -77.23553,3.7371 z"
57
     id="path4138" />
58
  <path
59
     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
60
     d="m 214.76931,324.51908 c 60.83777,-14.1145 111.89562,-31.6251 144.40025,-49.5229 3.12602,-1.7213 5.81747,-3.2537 5.98106,-3.4054 0.40534,-0.3759 -13.76388,-7.9415 -34.63489,-18.4929 -7.52161,-3.8026 -9.82337,-5.3787 -12.0735,-8.2668 -5.14485,-6.6036 -5.96081,-14.8404 -2.20331,-22.2417 1.80288,-3.5512 5.69484,-7.3007 9.36158,-9.019 5.20851,-2.4407 1.18148,-2.2865 59.71223,-2.2865 l 52.81361,0 2.13233,-2.1984 c 2.78673,-2.8731 5.23414,-6.4981 8.23035,-12.1905 14.14966,-26.8827 26.71842,-78.3816 36.24347,-148.503303 0.76704,-5.6468 1.36194,-10.2983 1.32201,-10.3369 -0.0399,-0.038 -5.47754,2.9629 -12.08361,6.6697 l -12.01104,6.7396 -133.83068,137.037303 c -73.60688,75.3705 -134.81732,138.0567 -136.0232,139.3026 l -2.19251,2.2653 8.254,-1.8067 c 4.53969,-0.9937 12.01053,-2.6783 16.60185,-3.7435 z"
61
     id="path4136" />
62
  <path
63
     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
64
     d="m 202.72524,284.43588 c 69.93294,-70.1332 135.4799,-131.9279 213.46406,-201.244203 7.71421,-6.8568 14.50542,-12.9341 15.09155,-13.5052 0.9482,-0.9239 0.96778,-0.9811 0.17761,-0.5188 -77.96496,45.611803 -139.23519,88.710503 -166.72539,117.278203 -18.81811,19.5556 -50.35654,64.861 -80.96704,116.3104 -0.91787,1.5427 1.02249,-0.3323 18.95921,-18.3204 z"
65
     id="path4142" />
66
  <path
67
     style="fill:#000000"
68
     d=""
69
     id="path4140"
70
     inkscape:connector-curvature="0" />
71
</svg>
721
D src/main/resources/com/scrivenvar/logo128.png
Binary file
D src/main/resources/com/scrivenvar/logo16.png
Binary file
D src/main/resources/com/scrivenvar/logo256.png
Binary file
D src/main/resources/com/scrivenvar/logo32.png
Binary file
D src/main/resources/com/scrivenvar/logo512.png
Binary file
D src/main/resources/com/scrivenvar/logo64.png
Binary file
D src/main/resources/com/scrivenvar/messages.properties
1
# ########################################################################
2
# Main Application Window
3
# ########################################################################
4
5
# suppress inspection "UnusedProperty" for whole file
6
7
# The application title should exist only once in the entire code base.
8
# All other references should either refer to this value via the Messages
9
# class, or indirectly using ${Main.title}.
10
Main.title=Scrivenvar
11
12
Main.menu.file=_File
13
Main.menu.file.new=_New
14
Main.menu.file.open=_Open...
15
Main.menu.file.close=_Close
16
Main.menu.file.close_all=Close All
17
Main.menu.file.save=_Save
18
Main.menu.file.save_as=Save _As
19
Main.menu.file.save_all=Save A_ll
20
Main.menu.file.exit=E_xit
21
22
Main.menu.edit=_Edit
23
Main.menu.edit.copy.html=Copy _HTML
24
Main.menu.edit.undo=_Undo
25
Main.menu.edit.redo=_Redo
26
Main.menu.edit.cut=Cu_t
27
Main.menu.edit.copy=_Copy
28
Main.menu.edit.paste=_Paste
29
Main.menu.edit.selectAll=Select _All
30
Main.menu.edit.find=_Find
31
Main.menu.edit.find.next=Find _Next
32
Main.menu.edit.preferences=_Preferences
33
34
Main.menu.insert=_Insert
35
Main.menu.insert.blockquote=_Blockquote
36
Main.menu.insert.code=Inline _Code
37
Main.menu.insert.fenced_code_block=_Fenced Code Block
38
Main.menu.insert.fenced_code_block.prompt=Enter code here
39
Main.menu.insert.link=_Link...
40
Main.menu.insert.image=_Image...
41
Main.menu.insert.heading.1=Heading _1
42
Main.menu.insert.heading.1.prompt=heading 1
43
Main.menu.insert.heading.2=Heading _2
44
Main.menu.insert.heading.2.prompt=heading 2
45
Main.menu.insert.heading.3=Heading _3
46
Main.menu.insert.heading.3.prompt=heading 3
47
Main.menu.insert.unordered_list=_Unordered List
48
Main.menu.insert.ordered_list=_Ordered List
49
Main.menu.insert.horizontal_rule=_Horizontal Rule
50
51
Main.menu.format=Forma_t
52
Main.menu.format.bold=_Bold
53
Main.menu.format.italic=_Italic
54
Main.menu.format.superscript=Su_perscript
55
Main.menu.format.subscript=Su_bscript
56
Main.menu.format.strikethrough=Stri_kethrough
57
58
Main.menu.definition=_Definition
59
Main.menu.definition.create=_Create
60
Main.menu.definition.insert=_Insert
61
62
Main.menu.help=_Help
63
Main.menu.help.about=About ${Main.title}
64
65
# ########################################################################
66
# Status Bar
67
# ########################################################################
68
69
Main.status.text.offset=offset
70
Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
71
Main.status.state.default=OK
72
Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
73
Main.status.error.def.blank=Move the caret to a word before inserting a definition.
74
Main.status.error.def.empty=Create a definition before inserting a definition.
75
Main.status.error.def.missing=No definition value found for ''{0}''.
76
Main.status.error.r=Error with [{0}...]: {1}
77
78
# ########################################################################
79
# Preferences
80
# ########################################################################
81
82
Preferences.r=R
83
Preferences.r.script=Startup Script
84
Preferences.r.script.desc=Script runs prior to executing R statements within the document.
85
Preferences.r.directory=Working Directory
86
Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script.
87
Preferences.r.delimiter.began=Delimiter Prefix
88
Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions.
89
Preferences.r.delimiter.ended=Delimiter Suffix
90
Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions.
91
92
Preferences.images=Images
93
Preferences.images.directory=Relative Directory
94
Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths.
95
Preferences.images.suffixes=Extensions
96
Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces.
97
98
Preferences.definitions=Definitions
99
Preferences.definitions.path=File name
100
Preferences.definitions.path.desc=Absolute path to interpolated string definitions.
101
Preferences.definitions.delimiter.began=Delimiter Prefix
102
Preferences.definitions.delimiter.began.desc=Indicates when a definition key is starting.
103
Preferences.definitions.delimiter.ended=Delimiter Suffix
104
Preferences.definitions.delimiter.ended.desc=Indicates when a definition key is ending.
105
106
Preferences.fonts=Editor
107
Preferences.fonts.size_editor=Font Size
108
Preferences.fonts.size_editor.desc=Font size to use for the text editor.
109
110
# ########################################################################
111
# Definition Pane and its Tree View
112
# ########################################################################
113
114
Definition.menu.create=Create
115
Definition.menu.rename=Rename
116
Definition.menu.remove=Delete
117
Definition.menu.add.default=Undefined
118
119
# ########################################################################
120
# Failure messages with respect to YAML files.
121
# ########################################################################
122
yaml.error.open=Could not open YAML file (ensure non-empty file).
123
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
124
yaml.error.missing=Empty definition value for key ''{0}''.
125
yaml.error.tree.form=Unassigned definition near ''{0}''.
126
127
# ########################################################################
128
# File Editor
129
# ########################################################################
130
131
FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
132
FileEditor.loadFailed.title=Load
133
FileEditor.loadFailed.reason.permissions=File must be readable and writable.
134
FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
135
FileEditor.saveFailed.title=Save
136
137
# ########################################################################
138
# File Open
139
# ########################################################################
140
141
Dialog.file.choose.open.title=Open File
142
Dialog.file.choose.save.title=Save File
143
144
Dialog.file.choose.filter.title.source=Source Files
145
Dialog.file.choose.filter.title.definition=Definition Files
146
Dialog.file.choose.filter.title.xml=XML Files
147
Dialog.file.choose.filter.title.all=All Files
148
149
# ########################################################################
150
# Alert Dialog
151
# ########################################################################
152
153
Alert.file.close.title=Close
154
Alert.file.close.text=Save changes to {0}?
155
156
# ########################################################################
157
# Definition Pane
158
# ########################################################################
159
160
Pane.definition.node.root.title=Definitions
161
Pane.definition.button.create.label=_Create
162
Pane.definition.button.rename.label=_Rename
163
Pane.definition.button.delete.label=_Delete
164
Pane.definition.button.create.tooltip=Add new item (Insert)
165
Pane.definition.button.rename.tooltip=Rename selected item (F2)
166
Pane.definition.button.delete.tooltip=Delete selected items (Delete)
167
168
# Controls ###############################################################
169
170
# ########################################################################
171
# Browse File
172
# ########################################################################
173
174
BrowseFileButton.chooser.title=Browse for local file
175
BrowseFileButton.chooser.allFilesFilter=All Files
176
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
177
178
# Dialogs ################################################################
179
180
# ########################################################################
181
# Image
182
# ########################################################################
183
184
Dialog.image.title=Image
185
Dialog.image.chooser.imagesFilter=Images
186
Dialog.image.previewLabel.text=Markdown Preview\:
187
Dialog.image.textLabel.text=Alternate Text\:
188
Dialog.image.titleLabel.text=Title (tooltip)\:
189
Dialog.image.urlLabel.text=Image URL\:
190
191
# ########################################################################
192
# Hyperlink
193
# ########################################################################
194
195
Dialog.link.title=Link
196
Dialog.link.previewLabel.text=Markdown Preview\:
197
Dialog.link.textLabel.text=Link Text\:
198
Dialog.link.titleLabel.text=Title (tooltip)\:
199
Dialog.link.urlLabel.text=Link URL\:
200
201
# ########################################################################
202
# About
203
# ########################################################################
204
205
Dialog.about.title=About
206
Dialog.about.header=${Main.title}
207
Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
2081
D src/main/resources/com/scrivenvar/preview/webview.css
1
/* RESET ***/
2
html{box-sizing:border-box;font-size:12pt}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0}
3
4
/* BODY ***/
5
body {
6
  /* Must be bundled in JAR file. */
7
  font-family: "Vollkorn", serif;
8
  background-color: #fff;
9
  margin: 0 auto;
10
  max-width: 960px;
11
  line-height: 1.6;
12
  color: #454545;
13
  padding: 0 1em;
14
  font-feature-settings: "liga" 1;
15
  font-variant-ligatures: normal;
16
}
17
18
body>*:first-child {
19
  margin-top: 0 !important;
20
}
21
22
body>*:last-child {
23
  margin-bottom: 0 !important;
24
}
25
26
/* BLOCKS ***/
27
p, blockquote, ul, ol, dl, table, pre {
28
  margin: 1em 0;
29
}
30
31
/* HEADINGS ***/
32
h1, h2, h3, h4, h5, h6 {
33
  font-weight: bold;
34
  margin: 1em 0 .5em;
35
}
36
37
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
38
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
39
  font-size: inherit;
40
}
41
42
h1 {
43
  font-size: 21pt;
44
}
45
46
h2 {
47
  font-size: 18pt;
48
  border-bottom: 1px solid #ccc;
49
}
50
51
h3 {
52
  font-size: 15pt;
53
}
54
55
h4 {
56
  font-size: 13.5pt;
57
}
58
59
h5 {
60
  font-size: 12pt;
61
}
62
63
h6 {
64
  font-size: 10.5pt;
65
}
66
67
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
68
  margin-top: .5em;
69
}
70
71
/* LINKS ***/
72
a {
73
  color: #0077aa;
74
  text-decoration: none;
75
}
76
77
a:hover {
78
  text-decoration: underline;
79
}
80
81
/* BULLET LISTS ***/
82
ul, ol {
83
  display: block;
84
  list-style: disc outside none;
85
  margin: 1em 0;
86
  padding: 0 0 0 2em;
87
}
88
89
ol {
90
  list-style-type: decimal;
91
}
92
93
ul ul, ol ul,
94
ol ol, ul ol {
95
  list-style-position: inside;
96
  margin-left: 1em;
97
}
98
99
ul ul, ol ul {
100
  list-style-type: circle;
101
}
102
103
ol ol, ul ol {
104
  list-style-type: lower-latin;
105
}
106
107
/* DEFINITION LISTS ***/
108
dl {
109
  /** Horizontal scroll bar will appear if set to 100%. */
110
  width: 99%;
111
  overflow: hidden;
112
  padding-left: 1em;
113
}
114
115
dl dt {
116
  font-weight: bold;
117
  float: left;
118
  width: 20%;
119
  clear: both;
120
  position: relative;
121
}
122
123
dl dd {
124
  float: right;
125
  width: 79%;
126
  padding-bottom: .5em;
127
  margin-left: 0;
128
}
129
130
/* CODE ***/
131
pre, code, tt {
132
  /* Must be bundled in JAR file. */
133
  font-family: "Fira Code", monospace;
134
  font-size: 10pt;
135
  background-color: #f8f8f8;
136
  text-decoration: none;
137
  white-space: pre-wrap;
138
  word-wrap: break-word;
139
  overflow-wrap: anywhere;
140
  border-radius: .125em;
141
}
142
143
code, tt {
144
  padding: .25em;
145
}
146
147
pre > code {
148
  /* Reset the padding. */
149
  padding: 0;
150
  border: none;
151
  background: transparent;
152
}
153
154
pre {
155
  border: .125em solid #ccc;
156
  overflow: auto;
157
  /* Assign the new padding, independently from previous. */
158
  padding: .25em .5em;
159
}
160
161
pre code, pre tt {
162
  background-color: transparent;
163
  border: none;
164
}
165
166
/* QUOTES ***/
167
blockquote {
168
  border-left: .25em solid #ccc;
169
  padding: 0 1em;
170
  color: #777;
171
}
172
173
blockquote>:first-child {
174
  margin-top: 0;
175
}
176
177
blockquote>:last-child {
178
  margin-bottom: 0;
179
}
180
181
/* HORIZONTAL RULES ***/
182
hr {
183
  clear: both;
184
  margin: 1.5em 0 1.5em;
185
  height: 0;
186
  overflow: hidden;
187
  border: none;
188
  background: transparent;
189
  border-bottom: .125em solid #ccc;
190
}
191
192
/* TABLES ***/
193
table {
194
  width: 100%;
195
}
196
197
tr:nth-child(odd) {
198
  background-color: #eee;
199
}
200
201
th {
202
  background-color: #454545;
203
  color: #fff;
204
}
205
206
th, td {
207
  text-align: left;
208
  padding: 0 1em;
209
}
210
211
/* IMAGES ***/
212
img {
213
  max-width: 100%;
214
}
215
216
/* Required for FlyingSaucer to detect the node.
217
 * See SVGReplacedElementFactory for details.
218
 */
219
tex {
220
  /* Ensure the formulas can be inlined with text. */
221
  display: inline-block;
222
}
223
224
/* Without a robust typesetting engine, there's no
225
 * nice-looking way to automatically typeset equations.
226
 * Sometimes baseline is appropriate, sometimes the
227
 * descender must be considered, and sometimes vertical
228
 * alignment to the middle looks best.
229
 */
230
p tex {
231
  vertical-align: baseline;
232
}
2331
D src/main/resources/com/scrivenvar/scene.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
/*---- toolbar ----*/
29
30
.tool-bar {
31
	-fx-spacing: 0;
32
}
33
34
.tool-bar .button {
35
	-fx-background-color: transparent;
36
}
37
38
.tool-bar .button:hover {
39
	-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
40
	-fx-color: -fx-hover-base;
41
}
42
43
.tool-bar .button:armed {
44
	-fx-color: -fx-pressed-base;
45
}
461
D src/main/resources/com/scrivenvar/settings.properties
1
# ########################################################################
2
# Application
3
# ########################################################################
4
5
application.title=scrivenvar
6
application.package=com/${application.title}
7
application.messages= com.${application.title}.messages
8
9
# Suppress multiple file modified notifications for one logical modification.
10
# Given in milliseconds.
11
application.watchdog.timeout=50
12
13
# ########################################################################
14
# Preferences
15
# ########################################################################
16
17
preferences.root=com.${application.title}
18
preferences.root.state=state
19
preferences.root.options=options
20
preferences.root.definition.source=definition.source
21
22
# ########################################################################
23
# File and Path References
24
# ########################################################################
25
file.stylesheet.scene=${application.package}/scene.css
26
file.stylesheet.markdown=${application.package}/editor/markdown.css
27
file.stylesheet.preview=webview.css
28
file.stylesheet.xml=${application.package}/xml.css
29
30
file.logo.16 =${application.package}/logo16.png
31
file.logo.32 =${application.package}/logo32.png
32
file.logo.128=${application.package}/logo128.png
33
file.logo.256=${application.package}/logo256.png
34
file.logo.512=${application.package}/logo512.png
35
36
# Default file name when a new file is created.
37
# This ensures that the file type can always be
38
# discerned so that the correct type of variable
39
# reference can be inserted.
40
file.default=untitled.md
41
file.definition.default=variables.yaml
42
43
# ########################################################################
44
# File name Extensions
45
# ########################################################################
46
47
# Comma-separated list of definition file name extensions.
48
definition.file.ext.json=*.json
49
definition.file.ext.toml=*.toml
50
definition.file.ext.yaml=*.yml,*.yaml
51
definition.file.ext.properties=*.properties,*.props
52
53
# Comma-separated list of file name extensions.
54
file.ext.rmarkdown=*.Rmd
55
file.ext.rxml=*.Rxml
56
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
57
file.ext.definition=${definition.file.ext.yaml}
58
file.ext.xml=*.xml,${file.ext.rxml}
59
file.ext.all=*.*
60
61
# File name extension search order for images.
62
file.ext.image.order=svg pdf png jpg tiff
63
64
# ########################################################################
65
# Variable Name Editor
66
# ########################################################################
67
68
# Maximum number of characters for a variable name. A variable is defined
69
# as one or more non-whitespace characters up to this maximum length.
70
editor.variable.maxLength=256
71
72
# ########################################################################
73
# Dialog Preferences
74
# ########################################################################
75
76
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
77
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
78
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
79
80
# Ensures a consistent button order for alert dialogs across platforms (because
81
# the default button order on Linux defies all logic).
82
dialog.alert.button.order=${dialog.alert.button.order.windows}
831
D src/main/resources/com/scrivenvar/variables.yaml
1
---
2
c:
3
  protagonist:
4
    name:
5
      First: Chloe
6
      First_pos: $c.protagonist.name.First$'s
7
      Middle: Irene
8
      Family: Angelos
9
      nick:
10
        Father: Savant
11
        Mother: Sweetie
12
    colour:
13
      eyes: green
14
      hair: dark auburn
15
      syn_1: black
16
      syn_2: purple
17
      syn_11: teal
18
      syn_6: silver
19
      favourite: emerald green
20
    speech:
21
      tic: oh
22
    father:
23
      heritage: Greek
24
      name:
25
        Short: Bryce
26
        First: Bryson
27
        First_pos: $c.protagonist.father.name.First$'s
28
        Honourific: Mr.
29
      education: Masters
30
      vocation:
31
        name: robotics
32
        title: roboticist
33
      employer:
34
        name:
35
          Short: Rabota
36
          Full: $c.protagonist.father.employer.name.Short$ Designs
37
      hair:
38
        style: thick, curly
39
        colour: black
40
      eyes:
41
        colour: dark brown
42
      Endear: Dad
43
      vehicle: coupé
44
    mother:
45
      name:
46
        Short: Cass
47
        First: Cassandra
48
        First_pos: $c.protagonist.mother.name.First$'s
49
        Honourific: Mrs.
50
      education: PhD
51
      speech:
52
        tic: cute
53
        Honorific: Doctor
54
      vocation:
55
        article: an
56
        name: oceanography
57
        title: oceanographer
58
      employer:
59
        name:
60
          Full: Oregon State University
61
          Short: OSU
62
      eyes:
63
        colour: blue
64
      hair:
65
        style: thick, curly
66
        colour: dark brown
67
      Endear: Mom
68
      Endear_pos: Mom's
69
    uncle:
70
      name:
71
        First: Damian
72
        First_pos: $c.protagonist.uncle.name.First$'s
73
        Family: Moros
74
      hands:
75
        fingers:
76
          shape: long, bony
77
    friend:
78
      primary:
79
        name:
80
          First: Gerard
81
          First_pos: $c.protagonist.friend.primary.name.First$'s
82
          Family: Baran
83
          Family_pos: $c.protagonist.friend.primary.name.Family$'s
84
        favourite:
85
          colour: midnight blue
86
        eyes:
87
          colour: hazel
88
        mother:
89
          name:
90
            First: Isabella
91
            Short: Izzy
92
            Honourific: Mrs.
93
        father:
94
          name:
95
            Short: Mo
96
            First: Montgomery
97
            First_pos: $c.protagonist.friend.primary.father.name.First$'s
98
            Honourific: Mr.
99
          speech:
100
            tic: y'know
101
          endear: Pops
102
  military:
103
    primary:
104
      name:
105
        First: Felix
106
        Family: LeMay
107
        Family_pos: LeMay's
108
      rank:
109
        Short: General
110
        Full: Brigadier $c.military.primary.rank.Short$
111
      colour:
112
        eyes: gray
113
        hair: dirty brown
114
    secondary:
115
      name:
116
        Family: Grell
117
      rank: Colonel
118
      colour:
119
        eyes: green
120
        hair: deep red
121
    quaternary:
122
      name:
123
        First: Gretchen
124
        Family: Steinherz
125
  minor:
126
    primary:
127
      name:
128
        First: River
129
        Family: Banks
130
        Honourific: Mx.
131
      vocation:
132
        title: salesperson
133
      employer:
134
        Name: Geophysical Prospecting Incorporated
135
        Abbr: GPI
136
        Area: Cold Spring Creek
137
        payment: twenty million
138
    secondary:
139
      name:
140
        First: Renato
141
        Middle: Carroña
142
        Family: Salvatierra
143
        Family_pos: $c.minor.secondary.name.Family$'s
144
        Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$
145
        Honourific: Mister
146
        Honourific_sp: Señor
147
      vocation:
148
        title: detective
149
    tertiary:
150
      name:
151
        First: Robert
152
        Family: Hanssen
153
154
  ai:
155
    protagonist:
156
      name:
157
        first: yoky
158
        First: Yoky
159
        First_pos: $c.ai.protagonist.name.First$'s
160
        Family: Tsukuda
161
        id: 46692
162
      persona:
163
        name:
164
          First: Hoshi
165
          First_pos: $c.ai.protagonist.persona.name.First$'s
166
          Family: Yamamoto
167
          Family_pos: $c.ai.protagonist.persona.name.Family$'s
168
      culture: Japanese-American
169
      ethnicity: Asian
170
      rank: Technical Sergeant
171
      speech:
172
        tic: okay
173
    first:
174
      Name: Prôtos
175
      Name_pos: Prôtos'
176
      age:
177
        actual: twenty-six weeks
178
        virtual: five years
179
    second:
180
      Name: Défteros
181
    third:
182
      Name: Trítos
183
    fourth:
184
      Name: Tétartos
185
    material:
186
      type: metal
187
      raw: ilmenite
188
      extract: ore
189
      name:
190
        short: titanium
191
        long: $c.ai.material.name.short$ dioxide
192
        Abbr: TiO~2~
193
      pejorative: tin
194
  animal:
195
    protagonist:
196
      Name: Trufflers
197
      type: pig
198
    antagonist:
199
      name: coywolf
200
      Name: Coywolf
201
      plural: coywolves
202
203
narrator:
204
  one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$)
205
  two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$)
206
207
military:
208
  name:
209
    Short: Agency
210
    Short_pos: $military.name.Short$'s
211
    plural: agencies
212
  machine:
213
    Name: Skopós
214
    Name_pos: $military.machine.Name$'
215
    Location: Arctic
216
    predictor: quantum chips
217
  land:
218
    name:
219
      Full: $military.name.Short$ of Defence
220
    Slogan: Safety in Numbers
221
  air:
222
    name:
223
      Full: $military.name.Short$ of Air
224
  compound:
225
    type: base
226
    lights:
227
      colour: blue
228
    nick:
229
      Prefix: Catacombs
230
      prep: of
231
      Suffix: Tartarus
232
233
government:
234
  Country: United States
235
236
location:
237
  protagonist:
238
    City: Corvallis
239
    Region: Oregon
240
    Geography: Willamette Valley
241
    secondary:
242
      City: Willow Branch Spring
243
      Region: Oregon
244
      Geography: Wheeler County
245
      Water: Clarno Rapids
246
      Road: Shaniko-Fossil Highway
247
    tertiary:
248
      City: Leavenworth
249
      Region: Washington
250
      Type: Bavarian village
251
    school:
252
      address: 1400 Northwest Buchanan Avenue
253
    hospital:
254
      Name: Good Samaritan Regional Medical Center
255
  ai:
256
    escape:
257
      country:
258
        Name: Ecuador
259
        Name_pos: Ecuador's
260
      mountain:
261
        Name: Chimborazo
262
263
language:
264
  ai:
265
    article: an
266
    singular: exanimis
267
    plural: exanimēs
268
    brain:
269
      singular: superum
270
      plural: supera
271
    title: memristor array
272
    Title: Memristor Array
273
  police:
274
    slang:
275
      singular: mippo
276
      plural: $language.police.slang.singular$s
277
278
date:
279
  anchor: 2042-09-02
280
  protagonist:
281
    born: 0
282
    conceived: -243
283
    attacked:
284
      first: 2192
285
      second: 8064
286
    father:
287
      attacked:
288
        first: -8205
289
      date:
290
        second: -1550
291
    family:
292
      moved:
293
        first: $date.protagonist.conceived$ + 35
294
  game:
295
    played:
296
      first: $date.protagonist.born$ - 672
297
      second: $date.protagonist.family.moved.first$ + 2
298
  ai:
299
    interviewed: 6198
300
    onboarded: $date.ai.interviewed$ + 290
301
    diagnosed: $date.ai.onboarded$ + 2
302
    resigned: $date.ai.diagnosed$ + 3
303
    trapped: $date.ai.resigned$ + 26
304
    torturer: $date.ai.trapped$ + 18
305
    memristor: $date.ai.torturer$ + 61
306
    ethics: $date.ai.memristor$ + 415
307
    trained: $date.ai.ethics$ + 385
308
    mindjacked: $date.ai.trained$ + 22
309
    bombed: $date.ai.mindjacked$ + 458
310
  military:
311
    machine:
312
      Construction: Six years
313
314
plot:
315
  Log: $c.ai.protagonist.name.First_pos$ Chronicles
316
  Channel: Quantum Channel
317
318
  device:
319
    computer:
320
      Name: Tau
321
    network:
322
      Name: Internet
323
    paper:
324
      name:
325
        full: electronic sheet
326
        short: sheet
327
    typewriter:
328
      Name: Underwood
329
      year: nineteen twenties
330
      room: root cellar
331
    portable:
332
      name: nanobook
333
    vehicle:
334
      name: robocars
335
      Name: Robocars
336
    sensor:
337
      name: BMP1580
338
    phone:
339
      name: comm
340
      name_pos: $plot.device.phone.name$'s
341
      Name: Comm
342
      plural: $plot.device.phone.name$s
343
    video:
344
      name: vidfeed
345
      plural: $plot.device.video.name$s
346
    game:
347
      Name: Psynæris
348
      thought: transed
349
      machine: telecognos
350
      location:
351
        Building: Nijō Castle
352
        District: Gion
353
        City: Kyoto
354
        Country: Japan
355
356
farm:
357
  population:
358
    estimate: 350
359
    actual: 1,000
360
  energy: 9800kJ
361
  width: 55m
362
  length: 55m
363
  storeys: 10
364
365
lamp:
366
  height: 0.17m
367
  length: 1.22m
368
  width: 0.28m
369
370
crop:
371
  name: 
372
    singular: tomato
373
    plural: $crop.name.singular$es
374
  energy: 318kJ
375
  weight: 450g
376
  yield: 50
377
  harvests: 7
378
  diameter: 2m
379
  height: 1.5m
380
381
heading:
382
  ch_01: Till
383
  ch_02: Sow
384
  ch_03: Seed
385
  ch_04: Germinate
386
  ch_05: Grow
387
  ch_06: Shoot
388
  ch_07: Bud
389
  ch_08: Bloom
390
  ch_09: Pollinate
391
  ch_10: Fruit
392
  ch_11: Harvest
393
  ch_12: Deliver
394
  ch_13: Spoil
395
  ch_14: Revolt
396
  ch_15: Compost
397
  ch_16: Burn
398
  ch_17: Release
399
  ch_18: End Notes
400
  ch_19: Characters
401
402
inference:
403
  unit: per cent
404
  min: two
405
  ch_sow: eighty
406
  ch_seed: fifty-two
407
  ch_germinate: thirty-one
408
  ch_grow: fifteen
409
  ch_shoot: seven
410
  ch_bloom: four
411
  ch_pollinate: two
412
  ch_harvest: ninety-five
413
  ch_delivery: ninety-eight
414
415
link:
416
  tartarus: https://en.wikipedia.org/wiki/Tartarus
417
  exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls
418
  atalanta: https://en.wikipedia.org/wiki/Atalanta
419
  detain: https://goo.gl/RCNuOQ
420
  ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics
421
  algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon
422
  holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust
423
  memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf
424
  surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487
425
  tor: https://www.torproject.org
426
  hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra
427
  foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134
428
  drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist
429
  fermi: https://arxiv.org/pdf/1404.0204v1.pdf
430
  face: https://www.youtube.com/watch?v=ladqJQLR2bA
431
  expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures
432
  governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531
433
  asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
434
  clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws
435
  jetpack: http://jetpackaviation.com/
436
  hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ
437
  eyes_five: https://en.wikipedia.org/wiki/Five_Eyes
438
  eyes_nine: https://www.privacytools.io/
439
  eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html
440
  tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml
441
4421
D src/main/resources/com/scrivenvar/xml.css
1
.tagmark {
2
    -fx-fill: gray;
3
}
4
.anytag {
5
    -fx-fill: crimson;
6
}
7
.paren {
8
    -fx-fill: firebrick;
9
    -fx-font-weight: bold;
10
}
11
.attribute {
12
    -fx-fill: darkviolet;
13
}
14
.avalue {
15
    -fx-fill: black;
16
}
171
18
.comment {
19
	-fx-fill: teal;
20
}
A src/test/java/com/keenwrite/tex/TeXRasterization.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.tex;
29
30
import com.whitemagicsoftware.tex.DefaultTeXFont;
31
import com.whitemagicsoftware.tex.TeXEnvironment;
32
import com.whitemagicsoftware.tex.TeXFormula;
33
import com.whitemagicsoftware.tex.TeXLayout;
34
import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D;
35
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
36
import com.whitemagicsoftware.tex.graphics.SvgGraphics2D;
37
import org.junit.jupiter.api.Test;
38
import org.xml.sax.SAXException;
39
40
import javax.imageio.ImageIO;
41
import javax.xml.parsers.DocumentBuilderFactory;
42
import javax.xml.parsers.ParserConfigurationException;
43
import java.awt.image.BufferedImage;
44
import java.io.ByteArrayInputStream;
45
import java.io.File;
46
import java.io.IOException;
47
import java.nio.file.Path;
48
49
import static com.keenwrite.preview.SvgRasterizer.*;
50
import static java.lang.System.getProperty;
51
import static org.junit.jupiter.api.Assertions.assertEquals;
52
53
/**
54
 * Test that TeX rasterization produces a readable image.
55
 */
56
public class TeXRasterization {
57
  private static final String LOAD_EXTERNAL_DTD =
58
      "http://apache.org/xml/features/nonvalidating/load-external-dtd";
59
60
  private static final String EQUATION =
61
      "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}";
62
63
  private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );
64
65
  private static final long FILESIZE = 12364;
66
67
  /**
68
   * Test that an equation can be converted to a raster image and the
69
   * final raster image size corresponds to the input equation. This is
70
   * a simple way to verify that the rasterization process is correct,
71
   * albeit if any aspect of the SVG algorithm changes (such as padding
72
   * around the equation), it will cause this test to fail, which is a bit
73
   * misleading.
74
   */
75
  @Test
76
  public void test_Rasterize_SimpleFormula_CorrectImageSize()
77
      throws IOException {
78
    final var g = new SvgGraphics2D();
79
    drawGraphics( g );
80
    verifyImage( rasterizeString( g.toString() ) );
81
  }
82
83
  /**
84
   * Test that an SVG document object model can be parsed and rasterized into
85
   * an image.
86
   */
87
  @Test
88
  public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage()
89
      throws ParserConfigurationException, IOException, SAXException {
90
    final var g = new SvgGraphics2D();
91
    drawGraphics( g );
92
93
    final var expectedSvg = g.toString();
94
    final var bytes = expectedSvg.getBytes();
95
96
    final var dbf = DocumentBuilderFactory.newInstance();
97
    dbf.setFeature( LOAD_EXTERNAL_DTD, false );
98
    dbf.setNamespaceAware( false );
99
    final var builder = dbf.newDocumentBuilder();
100
101
    final var doc = builder.parse( new ByteArrayInputStream( bytes ) );
102
    final var actualSvg = toSvg( doc.getDocumentElement() );
103
104
    verifyImage( rasterizeString( actualSvg ) );
105
  }
106
107
  /**
108
   * Test that an SVG image from a DOM element can be rasterized.
109
   *
110
   * @throws IOException Could not write the image.
111
   */
112
  @Test
113
  public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage()
114
      throws IOException {
115
    final var g = new SvgDomGraphics2D();
116
    drawGraphics( g );
117
118
    final var dom = g.toDom();
119
120
    verifyImage( rasterize( dom ) );
121
  }
122
123
  /**
124
   * Asserts that the given image matches an expected file size.
125
   *
126
   * @param image The image to check against the file size.
127
   * @throws IOException Could not write the image.
128
   */
129
  private void verifyImage( final BufferedImage image ) throws IOException {
130
    final var file = export( image, "dom.png" );
131
    assertEquals( FILESIZE, file.length() );
132
  }
133
134
  /**
135
   * Creates an SVG string for the default equation and font size.
136
   */
137
  private void drawGraphics( final AbstractGraphics2D g ) {
138
    final var size = 100f;
139
    final var texFont = new DefaultTeXFont( size );
140
    final var env = new TeXEnvironment( texFont );
141
    g.scale( size, size );
142
143
    final var formula = new TeXFormula( EQUATION );
144
    final var box = formula.createBox( env );
145
    final var layout = new TeXLayout( box, size );
146
147
    g.initialize( layout.getWidth(), layout.getHeight() );
148
    box.draw( g, layout.getX(), layout.getY() );
149
  }
150
151
  @SuppressWarnings("SameParameterValue")
152
  private File export( final BufferedImage image, final String filename )
153
      throws IOException {
154
    final var path = Path.of( DIR_TEMP, filename );
155
    final var file = path.toFile();
156
    ImageIO.write( image, "png", file );
157
    file.deleteOnExit();
158
    return file;
159
  }
160
}
1161
D src/test/java/com/scrivenvar/tex/TeXRasterization.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.tex;
29
30
import com.whitemagicsoftware.tex.DefaultTeXFont;
31
import com.whitemagicsoftware.tex.TeXEnvironment;
32
import com.whitemagicsoftware.tex.TeXFormula;
33
import com.whitemagicsoftware.tex.TeXLayout;
34
import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D;
35
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
36
import com.whitemagicsoftware.tex.graphics.SvgGraphics2D;
37
import org.junit.jupiter.api.Test;
38
import org.xml.sax.SAXException;
39
40
import javax.imageio.ImageIO;
41
import javax.xml.parsers.DocumentBuilderFactory;
42
import javax.xml.parsers.ParserConfigurationException;
43
import java.awt.image.BufferedImage;
44
import java.io.ByteArrayInputStream;
45
import java.io.File;
46
import java.io.IOException;
47
import java.nio.file.Path;
48
49
import static com.scrivenvar.preview.SvgRasterizer.*;
50
import static java.lang.System.getProperty;
51
import static org.junit.jupiter.api.Assertions.assertEquals;
52
53
/**
54
 * Test that TeX rasterization produces a readable image.
55
 */
56
public class TeXRasterization {
57
  private static final String LOAD_EXTERNAL_DTD =
58
      "http://apache.org/xml/features/nonvalidating/load-external-dtd";
59
60
  private static final String EQUATION =
61
      "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}";
62
63
  private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );
64
65
  private static final long FILESIZE = 12547;
66
67
  /**
68
   * Test that an equation can be converted to a raster image and the
69
   * final raster image size corresponds to the input equation. This is
70
   * a simple way to verify that the rasterization process is correct,
71
   * albeit if any aspect of the SVG algorithm changes (such as padding
72
   * around the equation), it will cause this test to fail, which is a bit
73
   * misleading.
74
   */
75
  @Test
76
  public void test_Rasterize_SimpleFormula_CorrectImageSize()
77
      throws IOException {
78
    final var g = new SvgGraphics2D();
79
    drawGraphics( g );
80
    verifyImage( rasterizeString( g.toString() ) );
81
  }
82
83
  /**
84
   * Test that an SVG document object model can be parsed and rasterized into
85
   * an image.
86
   */
87
  @Test
88
  public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage()
89
      throws ParserConfigurationException, IOException, SAXException {
90
    final var g = new SvgGraphics2D();
91
    drawGraphics( g );
92
93
    final var expectedSvg = g.toString();
94
    final var bytes = expectedSvg.getBytes();
95
96
    final var dbf = DocumentBuilderFactory.newInstance();
97
    dbf.setFeature( LOAD_EXTERNAL_DTD, false );
98
    dbf.setNamespaceAware( false );
99
    final var builder = dbf.newDocumentBuilder();
100
101
    final var doc = builder.parse( new ByteArrayInputStream( bytes ) );
102
    final var actualSvg = toSvg( doc.getDocumentElement() );
103
104
    verifyImage( rasterizeString( actualSvg ) );
105
  }
106
107
  /**
108
   * Test that an SVG image from a DOM element can be rasterized.
109
   *
110
   * @throws IOException Could not write the image.
111
   */
112
  @Test
113
  public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage()
114
      throws IOException {
115
    final var g = new SvgDomGraphics2D();
116
    drawGraphics( g );
117
118
    final var dom = g.toDom();
119
120
    verifyImage( rasterize( dom ) );
121
  }
122
123
  /**
124
   * Asserts that the given image matches an expected file size.
125
   *
126
   * @param image The image to check against the file size.
127
   * @throws IOException Could not write the image.
128
   */
129
  private void verifyImage( final BufferedImage image ) throws IOException {
130
    final var file = export( image, "dom.png" );
131
    assertEquals( FILESIZE, file.length() );
132
  }
133
134
  /**
135
   * Creates an SVG string for the default equation and font size.
136
   */
137
  private void drawGraphics( final AbstractGraphics2D g ) {
138
    final var size = 100f;
139
    final var texFont = new DefaultTeXFont( size );
140
    final var env = new TeXEnvironment( texFont );
141
    g.scale( size, size );
142
143
    final var formula = new TeXFormula( EQUATION );
144
    final var box = formula.createBox( env );
145
    final var layout = new TeXLayout( box, size );
146
147
    g.initialize( layout.getWidth(), layout.getHeight() );
148
    box.draw( g, layout.getX(), layout.getY() );
149
  }
150
151
  @SuppressWarnings("SameParameterValue")
152
  private File export( final BufferedImage image, final String filename )
153
      throws IOException {
154
    final var path = Path.of( DIR_TEMP, filename );
155
    final var file = path.toFile();
156
    ImageIO.write( image, "png", file );
157
    file.deleteOnExit();
158
    return file;
159
  }
160
}
1611