Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
.gitignore
dist
-scrivenvar.bin
-scrivenvar.exe
+*.bin
+*.exe
build
.gradle
BUILD.md
After the application is compiled, run it as follows:
- java -jar build/libs/scrivenvar.jar
+ java -jar build/libs/keenwrite.jar
On Windows:
- java -jar build\libs\scrivenvar.jar
+ java -jar build\libs\keenwrite.jar
# Installers
README.md
-# ![Logo](images/logo64.png) Scrivenvar
+# ![Logo](images/logo64.png) KeenWrite
A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values.
## Download
Download one of the following editions:
-* [Windows](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.exe)
-* [Linux](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.bin)
-* [Java Archive](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.jar)
+* [Windows](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.exe)
+* [Linux](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.bin)
+* [Java Archive](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.jar)
## Run
When upgrading to a new version, delete the following directory;
- C:\Users\%USERNAME%\AppData\Local\warp\packages\scrivenvar.exe
+ C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe
### Linux
-On Linux, run `chmod +x scrivenvar.bin` then `./scrivenvar.bin`.
+On Linux, run `chmod +x keenwrite.bin` then `./keenwrite.bin`.
### Other
On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/) that includes JavaFX module support, then run:
``` bash
-java -jar scrivenvar.jar
+java -jar keenwrite.jar
```
_config.yaml
---
application:
- title: "Scrivenvar"
+ title: "KeenWrite"
build.gradle
+import org.yaml.snakeyaml.Yaml
+
+buildscript {
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath group: 'org.yaml', name: 'snakeyaml', version: '1.19'
+ }
+}
+
plugins {
id 'application'
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
+
+def config = new Yaml().load( new File("_config.yaml").newInputStream() )
application {
- applicationName = 'scrivenvar'
+ applicationName = config["application"]["title"].toLowerCase()
mainClassName = "com.${applicationName}.Main"
applicationDefaultJvmArgs = [
"--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED",
"--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED",
"--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED",
]
}
+
+println applicationName
version = gitVersion()
installer
set SCRIPT_DIR=%~dp0
-"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" -jar "%SCRIPT_DIR%\\scrivenvar.jar" %*
+"%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" -jar "%SCRIPT_DIR%\\${APP_NAME}.jar" %*
__EOT
src/main/java/com/keenwrite/AbstractFileFactory.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import com.keenwrite.service.Settings;
+import com.keenwrite.util.ProtocolScheme;
+
+import java.nio.file.Path;
+
+import static com.keenwrite.Constants.GLOB_PREFIX_FILE;
+import static com.keenwrite.Constants.SETTINGS;
+import static com.keenwrite.FileType.UNKNOWN;
+import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
+import static java.lang.String.format;
+
+/**
+ * Provides common behaviours for factories that instantiate classes based on
+ * file type.
+ */
+public class AbstractFileFactory {
+
+ private static final String MSG_UNKNOWN_FILE_TYPE =
+ "Unknown type '%s' for file '%s'.";
+
+ /**
+ * Determines the file type from the path extension. This should only be
+ * called when it is known that the file type won't be a definition file
+ * (e.g., YAML or other definition source), but rather an editable file
+ * (e.g., Markdown, XML, etc.).
+ *
+ * @param path The path with a file name extension.
+ * @return The FileType for the given path.
+ */
+ public FileType lookup( final Path path ) {
+ return lookup( path, GLOB_PREFIX_FILE );
+ }
+
+ /**
+ * Creates a file type that corresponds to the given path.
+ *
+ * @param path Reference to a variable definition file.
+ * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
+ * @return The file type that corresponds to the given path.
+ */
+ protected FileType lookup( final Path path, final String prefix ) {
+ assert path != null;
+ assert prefix != null;
+
+ final var settings = getSettings();
+ final var keys = settings.getKeys( prefix );
+
+ var found = false;
+ var fileType = UNKNOWN;
+
+ while( keys.hasNext() && !found ) {
+ final var key = keys.next();
+ final var patterns = settings.getStringSettingList( key );
+ final var predicate = createFileTypePredicate( patterns );
+
+ if( found = predicate.test( path.toFile() ) ) {
+ // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
+ // to a standard name (as defined in the settings.properties file).
+ final String suffix = key.replace( prefix + ".", "" );
+ fileType = FileType.from( suffix );
+ }
+ }
+
+ return fileType;
+ }
+
+ /**
+ * Throws IllegalArgumentException because the given path could not be
+ * recognized. This exists because
+ *
+ * @param type The detected path type (protocol, file extension, etc.).
+ * @param path The path to a source of definitions.
+ */
+ protected void unknownFileType(
+ final ProtocolScheme type, final String path ) {
+ final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
+ throw new IllegalArgumentException( msg );
+ }
+
+ /**
+ * Return the singleton Settings instance.
+ *
+ * @return A non-null instance.
+ */
+ private Settings getSettings() {
+ return SETTINGS;
+ }
+}
src/main/java/com/keenwrite/Constants.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import com.keenwrite.service.Settings;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/**
+ * Defines application-wide default values.
+ */
+public class Constants {
+
+ public static final Settings SETTINGS = Services.load( Settings.class );
+
+ /**
+ * Prevent instantiation.
+ */
+ private Constants() {
+ }
+
+ private static String get( final String key ) {
+ return SETTINGS.getSetting( key, "" );
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static int get( final String key, final int defaultValue ) {
+ return SETTINGS.getSetting( key, defaultValue );
+ }
+
+ // Bootstrapping...
+ public static final String SETTINGS_NAME =
+ "/com/keenwrite/settings.properties";
+
+ public static final String DEFINITION_NAME = "variables.yaml";
+
+ public static final String APP_TITLE = get( "application.title" );
+ public static final String APP_BUNDLE_NAME = get( "application.messages" );
+
+ // Prevent double events when updating files on Linux (save and timestamp).
+ public static final int APP_WATCHDOG_TIMEOUT = get(
+ "application.watchdog.timeout", 200 );
+
+ public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
+ public static final String STYLESHEET_MARKDOWN = get(
+ "file.stylesheet.markdown" );
+ public static final String STYLESHEET_PREVIEW = get(
+ "file.stylesheet.preview" );
+
+ public static final String FILE_LOGO_16 = get( "file.logo.16" );
+ public static final String FILE_LOGO_32 = get( "file.logo.32" );
+ public static final String FILE_LOGO_128 = get( "file.logo.128" );
+ public static final String FILE_LOGO_256 = get( "file.logo.256" );
+ public static final String FILE_LOGO_512 = get( "file.logo.512" );
+
+ public static final String PREFS_ROOT = get( "preferences.root" );
+ public static final String PREFS_STATE = get( "preferences.root.state" );
+
+ /**
+ * Refer to filename extension settings in the configuration file. Do not
+ * terminate these prefixes with a period.
+ */
+ public static final String GLOB_PREFIX_FILE = "file.ext";
+ public static final String GLOB_PREFIX_DEFINITION =
+ "definition." + GLOB_PREFIX_FILE;
+
+ /**
+ * Three parameters: line number, column number, and offset.
+ */
+ public static final String STATUS_BAR_LINE = "Main.status.line";
+
+ public static final String STATUS_BAR_OK = "Main.status.state.default";
+
+ /**
+ * Used to show an error while parsing, usually syntactical.
+ */
+ public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
+ public static final String STATUS_DEFINITION_BLANK = "Main.status.error.def.blank";
+ public static final String STATUS_DEFINITION_EMPTY = "Main.status.error.def.empty";
+
+ /**
+ * One parameter: the word under the cursor that could not be found.
+ */
+ public static final String STATUS_DEFINITION_MISSING = "Main.status.error.def.missing";
+
+ /**
+ * Used when creating flat maps relating to resolved variables.
+ */
+ public static final int DEFAULT_MAP_SIZE = 64;
+
+ /**
+ * Default image extension order to use when scanning.
+ */
+ public static final String PERSIST_IMAGES_DEFAULT =
+ get( "file.ext.image.order" );
+
+ /**
+ * Default working directory to use for R startup script.
+ */
+ public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
+
+ /**
+ * Default path to use for an untitled (pathless) file.
+ */
+ public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY );
+
+ /**
+ * Default starting delimiter for definition variables.
+ */
+ public static final String DEF_DELIM_BEGAN_DEFAULT = "${";
+
+ /**
+ * Default ending delimiter for definition variables.
+ */
+ public static final String DEF_DELIM_ENDED_DEFAULT = "}";
+
+ /**
+ * Default starting delimiter when inserting R variables.
+ */
+ public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
+
+ /**
+ * Default ending delimiter when inserting R variables.
+ */
+ public static final String R_DELIM_ENDED_DEFAULT = " )";
+
+ /**
+ * Resource directory where different language lexicons are located.
+ */
+ public static final String LEXICONS_DIRECTORY = "lexicons";
+
+ /**
+ * Used as the prefix for uniquely identifying HTML block elements, which
+ * helps coordinate scrolling the preview pane to where the user is typing.
+ */
+ public static final String PARAGRAPH_ID_PREFIX = "p-";
+
+ /**
+ * Absolute location of true type font files within the Java archive file.
+ */
+ public static final String FONT_DIRECTORY = "/fonts";
+
+ /**
+ * Default text editor font size, in points.
+ */
+ public static final float FONT_SIZE_EDITOR = 12f;
+}
src/main/java/com/keenwrite/FileEditorTab.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import com.keenwrite.editors.EditorPane;
+import com.keenwrite.editors.markdown.MarkdownEditorPane;
+import com.keenwrite.service.events.Notification;
+import com.keenwrite.service.events.Notifier;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.Scene;
+import javafx.scene.control.Tab;
+import javafx.scene.control.Tooltip;
+import javafx.scene.text.Text;
+import javafx.stage.Window;
+import org.fxmisc.flowless.VirtualizedScrollPane;
+import org.fxmisc.richtext.StyleClassedTextArea;
+import org.fxmisc.undo.UndoManager;
+import org.jetbrains.annotations.NotNull;
+import org.mozilla.universalchardet.UniversalDetector;
+
+import java.io.File;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.StatusBarNotifier.alert;
+import static com.keenwrite.StatusBarNotifier.getNotifier;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Locale.ENGLISH;
+import static javafx.application.Platform.runLater;
+
+/**
+ * Editor for a single file.
+ */
+public final class FileEditorTab extends Tab {
+
+ private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane();
+
+ private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
+ private final BooleanProperty canUndo = new SimpleBooleanProperty();
+ private final BooleanProperty canRedo = new SimpleBooleanProperty();
+
+ /**
+ * Character encoding used by the file (or default encoding if none found).
+ */
+ private Charset mEncoding = UTF_8;
+
+ /**
+ * File to load into the editor.
+ */
+ private Path mPath;
+
+ public FileEditorTab( final Path path ) {
+ setPath( path );
+
+ mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
+
+ setOnSelectionChanged( e -> {
+ if( isSelected() ) {
+ runLater( this::activated );
+ requestFocus();
+ }
+ } );
+ }
+
+ private void updateTab() {
+ setText( getTabTitle() );
+ setGraphic( getModifiedMark() );
+ setTooltip( getTabTooltip() );
+ }
+
+ /**
+ * Returns the base filename (without the directory names).
+ *
+ * @return The untitled text if the path hasn't been set.
+ */
+ private String getTabTitle() {
+ return getPath().getFileName().toString();
+ }
+
+ /**
+ * Returns the full filename represented by the path.
+ *
+ * @return The untitled text if the path hasn't been set.
+ */
+ private Tooltip getTabTooltip() {
+ final Path filePath = getPath();
+ return new Tooltip( filePath == null ? "" : filePath.toString() );
+ }
+
+ /**
+ * Returns a marker to indicate whether the file has been modified.
+ *
+ * @return "*" when the file has changed; otherwise null.
+ */
+ private Text getModifiedMark() {
+ return isModified() ? new Text( "*" ) : null;
+ }
+
+ /**
+ * Called when the user switches tab.
+ */
+ private void activated() {
+ // Tab is closed or no longer active.
+ if( getTabPane() == null || !isSelected() ) {
+ return;
+ }
+
+ // If the tab is devoid of content, load it.
+ if( getContent() == null ) {
+ readFile();
+ initLayout();
+ initUndoManager();
+ }
+ }
+
+ private void initLayout() {
+ setContent( getScrollPane() );
+ }
+
+ /**
+ * Tracks undo requests, but can only be called <em>after</em> load.
+ */
+ private void initUndoManager() {
+ final UndoManager<?> undoManager = getUndoManager();
+ undoManager.forgetHistory();
+
+ // Bind the editor undo manager to the properties.
+ mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
+ canUndo.bind( undoManager.undoAvailableProperty() );
+ canRedo.bind( undoManager.redoAvailableProperty() );
+ }
+
+ private void requestFocus() {
+ getEditorPane().requestFocus();
+ }
+
+ /**
+ * Searches from the caret position forward for the given string.
+ *
+ * @param needle The text string to match.
+ */
+ public void searchNext( final String needle ) {
+ final String haystack = getEditorText();
+ int index = haystack.indexOf( needle, getCaretPosition() );
+
+ // Wrap around.
+ if( index == -1 ) {
+ index = haystack.indexOf( needle );
+ }
+
+ if( index >= 0 ) {
+ setCaretPosition( index );
+ getEditor().selectRange( index, index + needle.length() );
+ }
+ }
+
+ /**
+ * Gets a reference to the scroll pane that houses the editor.
+ *
+ * @return The editor's scroll pane, containing a vertical scrollbar.
+ */
+ public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
+ return getEditorPane().getScrollPane();
+ }
+
+ /**
+ * Returns the index into the text where the caret blinks happily away.
+ *
+ * @return A number from 0 to the editor's document text length.
+ */
+ public int getCaretPosition() {
+ return getEditor().getCaretPosition();
+ }
+
+ /**
+ * Moves the caret to a given offset.
+ *
+ * @param offset The new caret offset.
+ */
+ private void setCaretPosition( final int offset ) {
+ getEditor().moveTo( offset );
+ getEditor().requestFollowCaret();
+ }
+
+ /**
+ * Returns the text area associated with this tab.
+ *
+ * @return A text editor.
+ */
+ private StyleClassedTextArea getEditor() {
+ return getEditorPane().getEditor();
+ }
+
+ /**
+ * Returns true if the given path exactly matches this tab's path.
+ *
+ * @param check The path to compare against.
+ * @return true The paths are the same.
+ */
+ public boolean isPath( final Path check ) {
+ final Path filePath = getPath();
+
+ return filePath != null && filePath.equals( check );
+ }
+
+ /**
+ * Reads the entire file contents from the path associated with this tab.
+ */
+ private void readFile() {
+ final Path path = getPath();
+ final File file = path.toFile();
+
+ try {
+ if( file.exists() ) {
+ if( file.canWrite() && file.canRead() ) {
+ final EditorPane pane = getEditorPane();
+ pane.setText( asString( Files.readAllBytes( path ) ) );
+ pane.scrollToTop();
+ }
+ else {
+ final String msg = get( "FileEditor.loadFailed.reason.permissions" );
+ alert( "FileEditor.loadFailed.message", file.toString(), msg );
+ }
+ }
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+
+ /**
+ * Saves the entire file contents from the path associated with this tab.
+ *
+ * @return true The file has been saved.
+ */
+ public boolean save() {
+ try {
+ final EditorPane editor = getEditorPane();
+ Files.write( getPath(), asBytes( editor.getText() ) );
+ editor.getUndoManager().mark();
+ return true;
+ } catch( final Exception ex ) {
+ return popupAlert(
+ "FileEditor.saveFailed.title",
+ "FileEditor.saveFailed.message",
+ ex
+ );
+ }
+ }
+
+ /**
+ * Creates an alert dialog and waits for it to close.
+ *
+ * @param titleKey Resource bundle key for the alert dialog title.
+ * @param messageKey Resource bundle key for the alert dialog message.
+ * @param e The unexpected happening.
+ * @return false
+ */
+ @SuppressWarnings("SameParameterValue")
+ private boolean popupAlert(
+ final String titleKey, final String messageKey, final Exception e ) {
+ final Notifier service = getNotifier();
+ final Path filePath = getPath();
+
+ final Notification message = service.createNotification(
+ get( titleKey ),
+ get( messageKey ),
+ filePath == null ? "" : filePath,
+ e.getMessage()
+ );
+
+ try {
+ service.createError( getWindow(), message ).showAndWait();
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+
+ return false;
+ }
+
+ private Window getWindow() {
+ final Scene scene = getEditorPane().getScene();
+
+ if( scene == null ) {
+ throw new UnsupportedOperationException( "No scene window available" );
+ }
+
+ return scene.getWindow();
+ }
+
+ /**
+ * Returns a best guess at the file encoding. If the encoding could not be
+ * detected, this will return the default charset for the JVM.
+ *
+ * @param bytes The bytes to perform character encoding detection.
+ * @return The character encoding.
+ */
+ private Charset detectEncoding( final byte[] bytes ) {
+ final var detector = new UniversalDetector( null );
+ detector.handleData( bytes, 0, bytes.length );
+ detector.dataEnd();
+
+ final String charset = detector.getDetectedCharset();
+
+ return charset == null
+ ? Charset.defaultCharset()
+ : Charset.forName( charset.toUpperCase( ENGLISH ) );
+ }
+
+ /**
+ * Converts the given string to an array of bytes using the encoding that was
+ * originally detected (if any) and associated with this file.
+ *
+ * @param text The text to convert into the original file encoding.
+ * @return A series of bytes ready for writing to a file.
+ */
+ private byte[] asBytes( final String text ) {
+ return text.getBytes( getEncoding() );
+ }
+
+ /**
+ * Converts the given bytes into a Java String. This will call setEncoding
+ * with the encoding detected by the CharsetDetector.
+ *
+ * @param text The text of unknown character encoding.
+ * @return The text, in its auto-detected encoding, as a String.
+ */
+ private String asString( final byte[] text ) {
+ setEncoding( detectEncoding( text ) );
+ return new String( text, getEncoding() );
+ }
+
+ /**
+ * Returns the path to the file being edited in this tab.
+ *
+ * @return A non-null instance.
+ */
+ public Path getPath() {
+ return mPath;
+ }
+
+ /**
+ * Sets the path to a file for editing and then updates the tab with the
+ * file contents.
+ *
+ * @param path A non-null instance.
+ */
+ public void setPath( final Path path ) {
+ assert path != null;
+ mPath = path;
+
+ updateTab();
+ }
+
+ public boolean isModified() {
+ return mModified.get();
+ }
+
+ ReadOnlyBooleanProperty modifiedProperty() {
+ return mModified.getReadOnlyProperty();
+ }
+
+ BooleanProperty canUndoProperty() {
+ return this.canUndo;
+ }
+
+ BooleanProperty canRedoProperty() {
+ return this.canRedo;
+ }
+
+ private UndoManager<?> getUndoManager() {
+ return getEditorPane().getUndoManager();
+ }
+
+ /**
+ * Forwards to the editor pane's listeners for text change events.
+ *
+ * @param listener The listener to notify when the text changes.
+ */
+ public void addTextChangeListener( final ChangeListener<String> listener ) {
+ getEditorPane().addTextChangeListener( listener );
+ }
+
+ /**
+ * Forwards to the editor pane's listeners for caret change events.
+ *
+ * @param listener Notified when the caret position changes.
+ */
+ public void addCaretPositionListener(
+ final ChangeListener<? super Integer> listener ) {
+ getEditorPane().addCaretPositionListener( listener );
+ }
+
+ /**
+ * Forwards to the editor pane's listeners for paragraph index change events.
+ *
+ * @param listener Notified when the caret's paragraph index changes.
+ */
+ public void addCaretParagraphListener(
+ final ChangeListener<? super Integer> listener ) {
+ getEditorPane().addCaretParagraphListener( listener );
+ }
+
+ public <T extends Event> void addEventFilter(
+ final EventType<T> eventType,
+ final EventHandler<? super T> eventFilter ) {
+ getEditor().addEventFilter( eventType, eventFilter );
+ }
+
+ /**
+ * Forwards the request to the editor pane.
+ *
+ * @return The text to process.
+ */
+ public String getEditorText() {
+ return getEditorPane().getText();
+ }
+
+ /**
+ * Returns the editor pane, or creates one if it doesn't yet exist.
+ *
+ * @return The editor pane, never null.
+ */
+ @NotNull
+ public MarkdownEditorPane getEditorPane() {
+ return mEditorPane;
+ }
+
+ /**
+ * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
+ * determined.
+ *
+ * @return The file encoding or UTF-8 if unknown.
+ */
+ private Charset getEncoding() {
+ return mEncoding;
+ }
+
+ private void setEncoding( final Charset encoding ) {
+ assert encoding != null;
+ mEncoding = encoding;
+ }
+
+ /**
+ * Returns the tab title, without any modified indicators.
+ *
+ * @return The tab title.
+ */
+ @Override
+ public String toString() {
+ return getTabTitle();
+ }
+}
src/main/java/com/keenwrite/FileEditorTabPane.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import com.keenwrite.service.Options;
+import com.keenwrite.service.Settings;
+import com.keenwrite.service.events.Notification;
+import com.keenwrite.service.events.Notifier;
+import com.keenwrite.util.Utils;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.value.ChangeListener;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Tab;
+import javafx.scene.control.TabPane;
+import javafx.stage.FileChooser;
+import javafx.stage.FileChooser.ExtensionFilter;
+import javafx.stage.Window;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.prefs.Preferences;
+import java.util.stream.Collectors;
+
+import static com.keenwrite.Constants.GLOB_PREFIX_FILE;
+import static com.keenwrite.Constants.SETTINGS;
+import static com.keenwrite.FileType.*;
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
+import static com.keenwrite.service.events.Notifier.YES;
+
+/**
+ * Tab pane for file editors.
+ */
+public final class FileEditorTabPane extends TabPane {
+
+ private static final String FILTER_EXTENSION_TITLES =
+ "Dialog.file.choose.filter";
+
+ private static final Options sOptions = Services.load( Options.class );
+ private static final Notifier sNotifier = Services.load( Notifier.class );
+
+ private final ReadOnlyObjectWrapper<Path> mOpenDefinition =
+ new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
+ new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyBooleanWrapper mAnyFileEditorModified =
+ new ReadOnlyBooleanWrapper();
+ private final ChangeListener<Integer> mCaretPositionListener;
+ private final ChangeListener<Integer> mCaretParagraphListener;
+
+ /**
+ * Constructs a new file editor tab pane.
+ *
+ * @param caretPositionListener Listens for changes to caret position so
+ * that the status bar can update.
+ * @param caretParagraphListener Listens for changes to the caret's paragraph
+ * so that scrolling may occur.
+ */
+ public FileEditorTabPane(
+ final ChangeListener<Integer> caretPositionListener,
+ final ChangeListener<Integer> caretParagraphListener ) {
+ final ObservableList<Tab> tabs = getTabs();
+
+ setFocusTraversable( false );
+ setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
+
+ addTabSelectionListener(
+ ( tabPane, oldTab, newTab ) -> {
+ if( newTab != null ) {
+ mActiveFileEditor.set( (FileEditorTab) newTab );
+ }
+ }
+ );
+
+ final ChangeListener<Boolean> modifiedListener =
+ ( observable, oldValue, newValue ) -> {
+ for( final Tab tab : tabs ) {
+ if( ((FileEditorTab) tab).isModified() ) {
+ mAnyFileEditorModified.set( true );
+ break;
+ }
+ }
+ };
+
+ tabs.addListener(
+ (ListChangeListener<Tab>) change -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ change.getAddedSubList().forEach(
+ ( tab ) -> {
+ final var fet = (FileEditorTab) tab;
+ fet.modifiedProperty().addListener( modifiedListener );
+ } );
+ }
+ else if( change.wasRemoved() ) {
+ change.getRemoved().forEach(
+ ( tab ) -> {
+ final var fet = (FileEditorTab) tab;
+ fet.modifiedProperty().removeListener( modifiedListener );
+ }
+ );
+ }
+ }
+
+ // Changes in the tabs may also change anyFileEditorModified property
+ // (e.g. closed modified file)
+ modifiedListener.changed( null, null, null );
+ }
+ );
+
+ mCaretPositionListener = caretPositionListener;
+ mCaretParagraphListener = caretParagraphListener;
+ }
+
+ /**
+ * Allows observers to be notified when the current file editor tab changes.
+ *
+ * @param listener The listener to notify of tab change events.
+ */
+ public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
+ // Observe the tab so that when a new tab is opened or selected,
+ // a notification is kicked off.
+ getSelectionModel().selectedItemProperty().addListener( listener );
+ }
+
+ /**
+ * Returns the tab that has keyboard focus.
+ *
+ * @return A non-null instance.
+ */
+ public FileEditorTab getActiveFileEditor() {
+ return mActiveFileEditor.get();
+ }
+
+ /**
+ * Returns the property corresponding to the tab that has focus.
+ *
+ * @return A non-null instance.
+ */
+ public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
+ return mActiveFileEditor.getReadOnlyProperty();
+ }
+
+ /**
+ * Property that can answer whether the text has been modified.
+ *
+ * @return A non-null instance, true meaning the content has not been saved.
+ */
+ ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
+ return mAnyFileEditorModified.getReadOnlyProperty();
+ }
+
+ /**
+ * Creates a new editor instance from the given path.
+ *
+ * @param path The file to open.
+ * @return A non-null instance.
+ */
+ private FileEditorTab createFileEditor( final Path path ) {
+ assert path != null;
+
+ final FileEditorTab tab = new FileEditorTab( path );
+
+ tab.setOnCloseRequest( e -> {
+ if( !canCloseEditor( tab ) ) {
+ e.consume();
+ }
+ else if( isActiveFileEditor( tab ) ) {
+ // Prevent prompting the user to save when there are no file editor
+ // tabs open.
+ mActiveFileEditor.set( null );
+ }
+ } );
+
+ tab.addCaretPositionListener( mCaretPositionListener );
+ tab.addCaretParagraphListener( mCaretParagraphListener );
+
+ return tab;
+ }
+
+ private boolean isActiveFileEditor( final FileEditorTab tab ) {
+ return getActiveFileEditor() == tab;
+ }
+
+ private Path getDefaultPath() {
+ final String filename = getDefaultFilename();
+ return (new File( filename )).toPath();
+ }
+
+ private String getDefaultFilename() {
+ return getSettings().getSetting( "file.default", "untitled.md" );
+ }
+
+ /**
+ * Called to add a new {@link FileEditorTab} to the tab pane.
+ */
+ void newEditor() {
+ final FileEditorTab tab = createFileEditor( getDefaultPath() );
+
+ getTabs().add( tab );
+ getSelectionModel().select( tab );
+ }
+
+ void openFileDialog() {
+ final String title = get( "Dialog.file.choose.open.title" );
+ final FileChooser dialog = createFileChooser( title );
+ final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
+
+ if( files != null ) {
+ openFiles( files );
+ }
+ }
+
+ /**
+ * Opens the files into new editors, unless one of those files was a
+ * definition file. The definition file is loaded into the definition pane,
+ * but only the first one selected (multiple definition files will result in a
+ * warning).
+ *
+ * @param files The list of non-definition files that the were requested to
+ * open.
+ */
+ private void openFiles( final List<File> files ) {
+ final List<String> extensions =
+ createExtensionFilter( DEFINITION ).getExtensions();
+ final var predicate = createFileTypePredicate( extensions );
+
+ // The user might have opened multiple definitions files. These will
+ // be discarded from the text editable files.
+ final var definitions
+ = files.stream().filter( predicate ).collect( Collectors.toList() );
+
+ // Create a modifiable list to remove any definition files that were
+ // opened.
+ final var editors = new ArrayList<>( files );
+
+ if( !editors.isEmpty() ) {
+ saveLastDirectory( editors.get( 0 ) );
+ }
+
+ editors.removeAll( definitions );
+
+ // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
+ if( !editors.isEmpty() ) {
+ openEditors( editors, 0 );
+ }
+
+ if( !definitions.isEmpty() ) {
+ openDefinition( definitions.get( 0 ) );
+ }
+ }
+
+ private void openEditors( final List<File> files, final int activeIndex ) {
+ final int fileTally = files.size();
+ final List<Tab> tabs = getTabs();
+
+ // Close single unmodified "Untitled" tab.
+ if( tabs.size() == 1 ) {
+ final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
+
+ if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
+ closeEditor( fileEditor, false );
+ }
+ }
+
+ for( int i = 0; i < fileTally; i++ ) {
+ final Path path = files.get( i ).toPath();
+
+ FileEditorTab fileEditorTab = findEditor( path );
+
+ // Only open new files.
+ if( fileEditorTab == null ) {
+ fileEditorTab = createFileEditor( path );
+ getTabs().add( fileEditorTab );
+ }
+
+ // Select the first file in the list.
+ if( i == activeIndex ) {
+ getSelectionModel().select( fileEditorTab );
+ }
+ }
+ }
+
+ /**
+ * Returns a property that changes when a new definition file is opened.
+ *
+ * @return The path to a definition file that was opened.
+ */
+ public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
+ return getOnOpenDefinitionFile().getReadOnlyProperty();
+ }
+
+ private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
+ return mOpenDefinition;
+ }
+
+ /**
+ * Called when the user has opened a definition file (using the file open
+ * dialog box). This will replace the current set of definitions for the
+ * active tab.
+ *
+ * @param definition The file to open.
+ */
+ private void openDefinition( final File definition ) {
+ // TODO: Prevent reading this file twice when a new text document is opened.
+ // (might be a matter of checking the value first).
+ getOnOpenDefinitionFile().set( definition.toPath() );
+ }
+
+ /**
+ * Called when the contents of the editor are to be saved.
+ *
+ * @param tab The tab containing content to save.
+ * @return true The contents were saved (or needn't be saved).
+ */
+ public boolean saveEditor( final FileEditorTab tab ) {
+ if( tab == null || !tab.isModified() ) {
+ return true;
+ }
+
+ return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
+ }
+
+ /**
+ * Opens the Save As dialog for the user to save the content under a new
+ * path.
+ *
+ * @param tab The tab with contents to save.
+ * @return true The contents were saved, or the tab was null.
+ */
+ public boolean saveEditorAs( final FileEditorTab tab ) {
+ if( tab == null ) {
+ return true;
+ }
+
+ getSelectionModel().select( tab );
+
+ final FileChooser fileChooser = createFileChooser( get(
+ "Dialog.file.choose.save.title" ) );
+ final File file = fileChooser.showSaveDialog( getWindow() );
+ if( file == null ) {
+ return false;
+ }
+
+ saveLastDirectory( file );
+ tab.setPath( file.toPath() );
+
+ return tab.save();
+ }
+
+ void saveAllEditors() {
+ for( final FileEditorTab fileEditor : getAllEditors() ) {
+ saveEditor( fileEditor );
+ }
+ }
+
+ /**
+ * Answers whether the file has had modifications. '
+ *
+ * @param tab THe tab to check for modifications.
+ * @return false The file is unmodified.
+ */
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ boolean canCloseEditor( final FileEditorTab tab ) {
+ final AtomicReference<Boolean> canClose = new AtomicReference<>();
+ canClose.set( true );
+
+ if( tab.isModified() ) {
+ final Notification message = getNotifyService().createNotification(
+ Messages.get( "Alert.file.close.title" ),
+ Messages.get( "Alert.file.close.text" ),
+ tab.getText()
+ );
+
+ final Alert confirmSave = getNotifyService().createConfirmation(
+ getWindow(), message );
+
+ final Optional<ButtonType> buttonType = confirmSave.showAndWait();
+
+ buttonType.ifPresent(
+ save -> canClose.set(
+ save == YES ? saveEditor( tab ) : save == ButtonType.NO
+ )
+ );
+ }
+
+ return canClose.get();
+ }
+
+ boolean closeEditor( final FileEditorTab tab, final boolean save ) {
+ if( tab == null ) {
+ return true;
+ }
+
+ if( save ) {
+ Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
+ Event.fireEvent( tab, event );
+
+ if( event.isConsumed() ) {
+ return false;
+ }
+ }
+
+ getTabs().remove( tab );
+
+ if( tab.getOnClosed() != null ) {
+ Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
+ }
+
+ return true;
+ }
+
+ boolean closeAllEditors() {
+ final FileEditorTab[] allEditors = getAllEditors();
+ final FileEditorTab activeEditor = getActiveFileEditor();
+
+ // try to save active tab first because in case the user decides to cancel,
+ // then it stays active
+ if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
+ return false;
+ }
+
+ // This should be called any time a tab changes.
+ persistPreferences();
+
+ // save modified tabs
+ for( int i = 0; i < allEditors.length; i++ ) {
+ final FileEditorTab fileEditor = allEditors[ i ];
+
+ if( fileEditor == activeEditor ) {
+ continue;
+ }
+
+ if( fileEditor.isModified() ) {
+ // activate the modified tab to make its modified content visible to
+ // the user
+ getSelectionModel().select( i );
+
+ if( !canCloseEditor( fileEditor ) ) {
+ return false;
+ }
+ }
+ }
+
+ // Close all tabs.
+ for( final FileEditorTab fileEditor : allEditors ) {
+ if( !closeEditor( fileEditor, false ) ) {
+ return false;
+ }
+ }
+
+ return getTabs().isEmpty();
+ }
+
+ private FileEditorTab[] getAllEditors() {
+ final ObservableList<Tab> tabs = getTabs();
+ final int length = tabs.size();
+ final FileEditorTab[] allEditors = new FileEditorTab[ length ];
+
+ for( int i = 0; i < length; i++ ) {
+ allEditors[ i ] = (FileEditorTab) tabs.get( i );
+ }
+
+ return allEditors;
+ }
+
+ /**
+ * Returns the file editor tab that has the given path.
+ *
+ * @return null No file editor tab for the given path was found.
+ */
+ private FileEditorTab findEditor( final Path path ) {
+ for( final Tab tab : getTabs() ) {
+ final FileEditorTab fileEditor = (FileEditorTab) tab;
+
+ if( fileEditor.isPath( path ) ) {
+ return fileEditor;
+ }
+ }
+
+ return null;
+ }
+
+ private FileChooser createFileChooser( String title ) {
+ final FileChooser fileChooser = new FileChooser();
+
+ fileChooser.setTitle( title );
+ fileChooser.getExtensionFilters().addAll(
+ createExtensionFilters() );
+
+ final String lastDirectory = getPreferences().get( "lastDirectory", null );
+ File file = new File( (lastDirectory != null) ? lastDirectory : "." );
+
+ if( !file.isDirectory() ) {
+ file = new File( "." );
+ }
+
+ fileChooser.setInitialDirectory( file );
+ return fileChooser;
+ }
+
+ private List<ExtensionFilter> createExtensionFilters() {
+ final List<ExtensionFilter> list = new ArrayList<>();
+
+ // TODO: Return a list of all properties that match the filter prefix.
+ // This will allow dynamic filters to be added and removed just by
+ // updating the properties file.
+ list.add( createExtensionFilter( ALL ) );
+ list.add( createExtensionFilter( SOURCE ) );
+ list.add( createExtensionFilter( DEFINITION ) );
+ list.add( createExtensionFilter( XML ) );
+ return list;
+ }
+
+ /**
+ * Returns a filter for file name extensions recognized by the application
+ * that can be opened by the user.
+ *
+ * @param filetype Used to find the globbing pattern for extensions.
+ * @return A filename filter suitable for use by a FileDialog instance.
+ */
+ private ExtensionFilter createExtensionFilter( final FileType filetype ) {
+ final String tKey = String.format( "%s.title.%s",
+ FILTER_EXTENSION_TITLES,
+ filetype );
+ final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
+
+ return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
+ }
+
+ private void saveLastDirectory( final File file ) {
+ getPreferences().put( "lastDirectory", file.getParent() );
+ }
+
+ public void initPreferences() {
+ int activeIndex = 0;
+
+ final Preferences preferences = getPreferences();
+ final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
+ final String activeFileName = preferences.get( "activeFile", null );
+
+ final List<File> files = new ArrayList<>( fileNames.length );
+
+ for( final String fileName : fileNames ) {
+ final File file = new File( fileName );
+
+ if( file.exists() ) {
+ files.add( file );
+
+ if( fileName.equals( activeFileName ) ) {
+ activeIndex = files.size() - 1;
+ }
+ }
+ }
+
+ if( files.isEmpty() ) {
+ newEditor();
+ }
+ else {
+ openEditors( files, activeIndex );
+ }
+ }
+
+ public void persistPreferences() {
+ final var allEditors = getTabs();
+ final List<String> fileNames = new ArrayList<>( allEditors.size() );
+
+ for( final var tab : allEditors ) {
+ final var fileEditor = (FileEditorTab) tab;
+ final var filePath = fileEditor.getPath();
+
+ if( filePath != null ) {
+ fileNames.add( filePath.toString() );
+ }
+ }
+
+ final var preferences = getPreferences();
+ Utils.putPrefsStrings( preferences,
+ "file",
+ fileNames.toArray( new String[ 0 ] ) );
+
+ final var activeEditor = getActiveFileEditor();
+ final var filePath = activeEditor == null ? null : activeEditor.getPath();
+
+ if( filePath == null ) {
+ preferences.remove( "activeFile" );
+ }
+ else {
+ preferences.put( "activeFile", filePath.toString() );
+ }
+ }
+
+ private List<String> getExtensions( final String key ) {
+ return getSettings().getStringSettingList( key );
+ }
+
+ private Notifier getNotifyService() {
+ return sNotifier;
+ }
+
+ private Settings getSettings() {
+ return SETTINGS;
+ }
+
+ protected Options getOptions() {
+ return sOptions;
+ }
+
+ private Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ private Preferences getPreferences() {
+ return getOptions().getState();
+ }
+}
src/main/java/com/keenwrite/FileType.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+/**
+ * Represents different file type classifications. These are high-level mappings
+ * that correspond to the list of glob patterns found within {@code
+ * settings.properties}.
+ */
+public enum FileType {
+
+ ALL( "all" ),
+ RMARKDOWN( "rmarkdown" ),
+ RXML( "rxml" ),
+ SOURCE( "source" ),
+ DEFINITION( "definition" ),
+ XML( "xml" ),
+ CSV( "csv" ),
+ JSON( "json" ),
+ TOML( "toml" ),
+ YAML( "yaml" ),
+ PROPERTIES( "properties" ),
+ UNKNOWN( "unknown" );
+
+ private final String mType;
+
+ /**
+ * Default constructor for enumerated file type.
+ *
+ * @param type Human-readable name for the file type.
+ */
+ FileType( final String type ) {
+ mType = type;
+ }
+
+ /**
+ * Returns the file type that corresponds to the given string.
+ *
+ * @param type The string to compare against this enumeration of file types.
+ * @return The corresponding File Type for the given string.
+ * @throws IllegalArgumentException Type not found.
+ */
+ public static FileType from( final String type ) {
+ for( final FileType fileType : FileType.values() ) {
+ if( fileType.isType( type ) ) {
+ return fileType;
+ }
+ }
+
+ throw new IllegalArgumentException( type );
+ }
+
+ /**
+ * Answers whether this file type matches the given string, case insensitive
+ * comparison.
+ *
+ * @param type Presumably a file name extension to check against.
+ * @return true The given extension corresponds to this enumerated type.
+ */
+ public boolean isType( final String type ) {
+ return getType().equalsIgnoreCase( type );
+ }
+
+ /**
+ * Returns the human-readable name for the file type.
+ *
+ * @return A non-null instance.
+ */
+ private String getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the lowercase version of the file name extension.
+ *
+ * @return The file name, in lower case.
+ */
+ @Override
+ public String toString() {
+ return getType();
+ }
+}
src/main/java/com/keenwrite/Launcher.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Calendar;
+import java.util.Properties;
+
+import static java.lang.String.format;
+
+/**
+ * Launches the application using the {@link Main} class.
+ *
+ * <p>
+ * This is required until modules are implemented, which may never happen
+ * because the application should be ported away from Java and JavaFX.
+ * </p>
+ */
+public class Launcher {
+ /**
+ * Delegates to the application entry point.
+ *
+ * @param args Command-line arguments.
+ */
+ public static void main( final String[] args ) throws IOException {
+ showAppInfo();
+ Main.main( args );
+ }
+
+ @SuppressWarnings("RedundantStringFormatCall")
+ private static void showAppInfo() throws IOException {
+ out( format( "%s version %s", getTitle(), getVersion() ) );
+ out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) );
+ out( format( "Portions copyright 2020 Karl Tauber." ) );
+ }
+
+ private static void out( final String s ) {
+ System.out.println( s );
+ }
+
+ private static String getTitle() throws IOException {
+ final Properties properties = loadProperties( "messages.properties" );
+ return properties.getProperty( "Main.title" );
+ }
+
+ private static String getVersion() throws IOException {
+ final Properties properties = loadProperties( "app.properties" );
+ return properties.getProperty( "application.version" );
+ }
+
+ private static String getYear() {
+ return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static Properties loadProperties( final String resource )
+ throws IOException {
+ final Properties properties = new Properties();
+ properties.load( getResourceAsStream( getResourceName( resource ) ) );
+ return properties;
+ }
+
+ private static String getResourceName( final String resource ) {
+ return format( "%s/%s", getPackagePath(), resource );
+ }
+
+ private static String getPackagePath() {
+ return Launcher.class.getPackageName().replace( '.', '/' );
+ }
+
+ private static InputStream getResourceAsStream( final String resource ) {
+ return Launcher.class.getClassLoader().getResourceAsStream( resource );
+ }
+}
src/main/java/com/keenwrite/Main.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import com.keenwrite.preferences.FilePreferencesFactory;
+import com.keenwrite.service.Options;
+import com.keenwrite.service.Snitch;
+import com.keenwrite.util.ResourceWalker;
+import com.keenwrite.util.StageState;
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.scene.image.Image;
+import javafx.stage.Stage;
+
+import java.awt.*;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.Map;
+import java.util.logging.LogManager;
+
+import static com.keenwrite.Constants.*;
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.StatusBarNotifier.alert;
+import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment;
+import static java.awt.font.TextAttribute.*;
+import static javafx.scene.input.KeyCode.F11;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+
+/**
+ * Application entry point. The application allows users to edit Markdown
+ * files and see a real-time preview of the edits.
+ */
+public final class Main extends Application {
+
+ static {
+ // Suppress logging to standard output.
+ LogManager.getLogManager().reset();
+
+ // Suppress logging to standard error.
+ System.err.close();
+ }
+
+ private final Options mOptions = Services.load( Options.class );
+ private final Snitch mSnitch = Services.load( Snitch.class );
+
+ private final Thread mSnitchThread = new Thread( getSnitch() );
+ private final MainWindow mMainWindow = new MainWindow();
+
+ @SuppressWarnings({"FieldCanBeLocal", "unused"})
+ private StageState mStageState;
+
+ /**
+ * Application entry point.
+ *
+ * @param args Command-line arguments.
+ */
+ public static void main( final String[] args ) {
+ initPreferences();
+ initFonts();
+ launch( args );
+ }
+
+ /**
+ * JavaFX entry point.
+ *
+ * @param stage The primary application stage.
+ */
+ @Override
+ public void start( final Stage stage ) {
+ initState( stage );
+ initStage( stage );
+ initSnitch();
+
+ stage.show();
+
+ // After the stage is visible, the panel dimensions are
+ // known, which allows scaling images to fit the preview panel.
+ getMainWindow().init();
+ }
+
+ /**
+ * This needs to run before the windowing system kicks in, otherwise the
+ * fonts will not be found.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public static void initFonts() {
+ final var ge = getLocalGraphicsEnvironment();
+
+ try {
+ ResourceWalker.walk(
+ FONT_DIRECTORY, path -> {
+ final var uri = path.toUri();
+ final var filename = path.toString();
+
+ try( final var is = openFont( uri, filename ) ) {
+ final var font = Font.createFont( Font.TRUETYPE_FONT, is );
+ final Map attributes = font.getAttributes();
+
+ attributes.put( LIGATURES, LIGATURES_ON );
+ attributes.put( KERNING, KERNING_ON );
+ ge.registerFont( font.deriveFont( attributes ) );
+ } catch( final Exception e ) {
+ alert( e );
+ }
+ }
+ );
+ } catch( final Exception e ) {
+ alert( e );
+ }
+ }
+
+ private static InputStream openFont( final URI uri, final String filename )
+ throws IOException {
+ return uri.getScheme().equals( "jar" )
+ ? Main.class.getResourceAsStream( filename )
+ : new FileInputStream( filename );
+ }
+
+ /**
+ * Sets the factory used for reading user preferences.
+ */
+ private static void initPreferences() {
+ System.setProperty(
+ "java.util.prefs.PreferencesFactory",
+ FilePreferencesFactory.class.getName()
+ );
+ }
+
+ private void initState( final Stage stage ) {
+ mStageState = new StageState( stage, getOptions().getState() );
+ }
+
+ private void initStage( final Stage stage ) {
+ stage.getIcons().addAll(
+ createImage( FILE_LOGO_16 ),
+ createImage( FILE_LOGO_32 ),
+ createImage( FILE_LOGO_128 ),
+ createImage( FILE_LOGO_256 ),
+ createImage( FILE_LOGO_512 ) );
+ stage.setTitle( getApplicationTitle() );
+ stage.setScene( getScene() );
+
+ stage.addEventHandler( KEY_PRESSED, event -> {
+ if( F11.equals( event.getCode() ) ) {
+ stage.setFullScreen( !stage.isFullScreen() );
+ }
+ } );
+ }
+
+ /**
+ * Watch for file system changes.
+ */
+ private void initSnitch() {
+ getSnitchThread().start();
+ }
+
+ /**
+ * Stops the snitch service, if its running.
+ *
+ * @throws InterruptedException Couldn't stop the snitch thread.
+ */
+ @Override
+ public void stop() throws InterruptedException {
+ getSnitch().stop();
+
+ final Thread thread = getSnitchThread();
+ thread.interrupt();
+ thread.join();
+ }
+
+ private Snitch getSnitch() {
+ return mSnitch;
+ }
+
+ private Thread getSnitchThread() {
+ return mSnitchThread;
+ }
+
+ private Options getOptions() {
+ return mOptions;
+ }
+
+ private MainWindow getMainWindow() {
+ return mMainWindow;
+ }
+
+ private Scene getScene() {
+ return getMainWindow().getScene();
+ }
+
+ private String getApplicationTitle() {
+ return get( "Main.title" );
+ }
+
+ private Image createImage( final String filename ) {
+ return new Image( filename );
+ }
+}
src/main/java/com/keenwrite/MainWindow.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import com.dlsc.preferencesfx.PreferencesFxEvent;
+import com.keenwrite.definition.DefinitionFactory;
+import com.keenwrite.definition.DefinitionPane;
+import com.keenwrite.definition.DefinitionSource;
+import com.keenwrite.definition.MapInterpolator;
+import com.keenwrite.definition.yaml.YamlDefinitionSource;
+import com.keenwrite.editors.DefinitionNameInjector;
+import com.keenwrite.editors.EditorPane;
+import com.keenwrite.editors.markdown.MarkdownEditorPane;
+import com.keenwrite.preferences.UserPreferences;
+import com.keenwrite.preview.HTMLPreviewPane;
+import com.keenwrite.processors.HtmlPreviewProcessor;
+import com.keenwrite.processors.Processor;
+import com.keenwrite.processors.ProcessorFactory;
+import com.keenwrite.service.Options;
+import com.keenwrite.service.Snitch;
+import com.keenwrite.spelling.api.SpellCheckListener;
+import com.keenwrite.spelling.api.SpellChecker;
+import com.keenwrite.spelling.impl.PermissiveSpeller;
+import com.keenwrite.spelling.impl.SymSpellSpeller;
+import com.keenwrite.util.Action;
+import com.keenwrite.util.ActionBuilder;
+import com.keenwrite.util.ActionUtils;
+import com.vladsch.flexmark.parser.Parser;
+import com.vladsch.flexmark.util.ast.NodeVisitor;
+import com.vladsch.flexmark.util.ast.VisitHandler;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+import javafx.util.Duration;
+import org.apache.commons.lang3.SystemUtils;
+import org.controlsfx.control.StatusBar;
+import org.fxmisc.richtext.StyleClassedTextArea;
+import org.fxmisc.richtext.model.StyleSpansBuilder;
+import org.reactfx.value.Val;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.prefs.Preferences;
+import java.util.stream.Collectors;
+
+import static com.keenwrite.Constants.*;
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.StatusBarNotifier.alert;
+import static com.keenwrite.util.StageState.*;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static javafx.application.Platform.runLater;
+import static javafx.event.Event.fireEvent;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
+import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ */
+public class MainWindow implements Observer {
+ /**
+ * The {@code OPTIONS} variable must be declared before all other variables
+ * to prevent subsequent initializations from failing due to missing user
+ * preferences.
+ */
+ private static final Options sOptions = Services.load( Options.class );
+ private static final Snitch SNITCH = Services.load( Snitch.class );
+
+ private final Scene mScene;
+ private final StatusBar mStatusBar;
+ private final Text mLineNumberText;
+ private final TextField mFindTextField;
+ private final SpellChecker mSpellChecker;
+
+ private final Object mMutex = new Object();
+
+ /**
+ * Prevents re-instantiation of processing classes.
+ */
+ private final Map<FileEditorTab, Processor<String>> mProcessors =
+ new HashMap<>();
+
+ private final Map<String, String> mResolvedMap =
+ new HashMap<>( DEFAULT_MAP_SIZE );
+
+ private final EventHandler<PreferencesFxEvent> mRPreferencesListener =
+ event -> rerender();
+
+ /**
+ * Called when the definition data is changed.
+ */
+ private final EventHandler<TreeItem.TreeModificationEvent<Event>>
+ mTreeHandler = event -> {
+ exportDefinitions( getDefinitionPath() );
+ interpolateResolvedMap();
+ rerender();
+ };
+
+ /**
+ * Called to inject the selected item when the user presses ENTER in the
+ * definition pane.
+ */
+ private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
+ event -> {
+ if( event.getCode() == ENTER ) {
+ getDefinitionNameInjector().injectSelectedItem();
+ }
+ };
+
+ private final ChangeListener<Integer> mCaretPositionListener =
+ ( observable, oldPosition, newPosition ) -> {
+ final FileEditorTab tab = getActiveFileEditorTab();
+ final EditorPane pane = tab.getEditorPane();
+ final StyleClassedTextArea editor = pane.getEditor();
+
+ getLineNumberText().setText(
+ get( STATUS_BAR_LINE,
+ editor.getCurrentParagraph() + 1,
+ editor.getParagraphs().size(),
+ editor.getCaretPosition()
+ )
+ );
+ };
+
+ private final ChangeListener<Integer> mCaretParagraphListener =
+ ( observable, oldIndex, newIndex ) ->
+ scrollToParagraph( newIndex, true );
+
+ private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
+ private final DefinitionPane mDefinitionPane = createDefinitionPane();
+ private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
+ private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
+ mCaretPositionListener,
+ mCaretParagraphListener );
+
+ /**
+ * Listens on the definition pane for double-click events.
+ */
+ private final DefinitionNameInjector mDefinitionNameInjector
+ = new DefinitionNameInjector( mDefinitionPane );
+
+ public MainWindow() {
+ mStatusBar = createStatusBar();
+ mLineNumberText = createLineNumberText();
+ mFindTextField = createFindTextField();
+ mScene = createScene();
+ mSpellChecker = createSpellChecker();
+
+ // Add the close request listener before the window is shown.
+ initLayout();
+ StatusBarNotifier.setStatusBar( mStatusBar );
+ }
+
+ /**
+ * Called after the stage is shown.
+ */
+ public void init() {
+ initFindInput();
+ initSnitch();
+ initDefinitionListener();
+ initTabAddedListener();
+ initTabChangedListener();
+ initPreferences();
+ initVariableNameInjector();
+ }
+
+ private void initLayout() {
+ final var scene = getScene();
+
+ scene.getStylesheets().add( STYLESHEET_SCENE );
+ scene.windowProperty().addListener(
+ ( unused, oldWindow, newWindow ) ->
+ newWindow.setOnCloseRequest(
+ e -> {
+ if( !getFileEditorPane().closeAllEditors() ) {
+ e.consume();
+ }
+ }
+ )
+ );
+ }
+
+ /**
+ * Initialize the find input text field to listen on F3, ENTER, and
+ * ESCAPE key presses.
+ */
+ private void initFindInput() {
+ final TextField input = getFindTextField();
+
+ input.setOnKeyPressed( ( KeyEvent event ) -> {
+ switch( event.getCode() ) {
+ case F3:
+ case ENTER:
+ editFindNext();
+ break;
+ case F:
+ if( !event.isControlDown() ) {
+ break;
+ }
+ case ESCAPE:
+ getStatusBar().setGraphic( null );
+ getActiveFileEditorTab().getEditorPane().requestFocus();
+ break;
+ }
+ } );
+
+ // Remove when the input field loses focus.
+ input.focusedProperty().addListener(
+ ( focused, oldFocus, newFocus ) -> {
+ if( !newFocus ) {
+ getStatusBar().setGraphic( null );
+ }
+ }
+ );
+ }
+
+ /**
+ * Watch for changes to external files. In particular, this awaits
+ * modifications to any XSL files associated with XML files being edited.
+ * When
+ * an XSL file is modified (external to the application), the snitch's ears
+ * perk up and the file is reloaded. This keeps the XSL transformation up to
+ * date with what's on the file system.
+ */
+ private void initSnitch() {
+ SNITCH.addObserver( this );
+ }
+
+ /**
+ * Listen for {@link FileEditorTabPane} to receive open definition file
+ * event.
+ */
+ private void initDefinitionListener() {
+ getFileEditorPane().onOpenDefinitionFileProperty().addListener(
+ ( final ObservableValue<? extends Path> file,
+ final Path oldPath, final Path newPath ) -> {
+ openDefinitions( newPath );
+ rerender();
+ }
+ );
+ }
+
+ /**
+ * Re-instantiates all processors then re-renders the active tab. This
+ * will refresh the resolved map, force R to re-initialize, and brute-force
+ * XSLT file reloads.
+ */
+ private void rerender() {
+ runLater(
+ () -> {
+ resetProcessors();
+ renderActiveTab();
+ }
+ );
+ }
+
+ /**
+ * When tabs are added, hook the various change listeners onto the new
+ * tab sothat the preview pane refreshes as necessary.
+ */
+ private void initTabAddedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Make sure the text processor kicks off when new files are opened.
+ final ObservableList<Tab> tabs = editorPane.getTabs();
+
+ // Update the preview pane on tab changes.
+ tabs.addListener(
+ ( final Change<? extends Tab> change ) -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ // Multiple tabs can be added simultaneously.
+ for( final Tab newTab : change.getAddedSubList() ) {
+ final FileEditorTab tab = (FileEditorTab) newTab;
+
+ initTextChangeListener( tab );
+ initScrollEventListener( tab );
+ initSpellCheckListener( tab );
+// initSyntaxListener( tab );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener(
+ ( __, ov, nv ) -> {
+ process( tab );
+ scrollToParagraph( getCurrentParagraphIndex() );
+ }
+ );
+ }
+
+ private void initScrollEventListener( final FileEditorTab tab ) {
+ final var scrollPane = tab.getScrollPane();
+ final var scrollBar = getPreviewPane().getVerticalScrollBar();
+
+ addShowListener( scrollPane, ( __ ) -> {
+ final var handler = new ScrollEventHandler( scrollPane, scrollBar );
+ handler.enabledProperty().bind( tab.selectedProperty() );
+ } );
+ }
+
+ /**
+ * Listen for changes to the any particular paragraph and perform a quick
+ * spell check upon it. The style classes in the editor will be changed to
+ * mark any spelling mistakes in the paragraph. The user may then interact
+ * with any misspelled word (i.e., any piece of text that is marked) to
+ * revise the spelling.
+ *
+ * @param tab The tab to spellcheck.
+ */
+ private void initSpellCheckListener( final FileEditorTab tab ) {
+ final var editor = tab.getEditorPane().getEditor();
+
+ // When the editor first appears, run a full spell check. This allows
+ // spell checking while typing to be restricted to the active paragraph,
+ // which is usually substantially smaller than the whole document.
+ addShowListener(
+ editor, ( __ ) -> spellcheck( editor, editor.getText() )
+ );
+
+ // Use the plain text changes so that notifications of style changes
+ // are suppressed. Checking against the identity ensures that only
+ // new text additions or deletions trigger proofreading.
+ editor.plainTextChanges()
+ .filter( p -> !p.isIdentity() ).subscribe( change -> {
+
+ // Only perform a spell check on the current paragraph. The
+ // entire document is processed once, when opened.
+ final var offset = change.getPosition();
+ final var position = editor.offsetToPosition( offset, Forward );
+ final var paraId = position.getMajor();
+ final var paragraph = editor.getParagraph( paraId );
+ final var text = paragraph.getText();
+
+ // Ensure that styles aren't doubled-up.
+ editor.clearStyle( paraId );
+
+ spellcheck( editor, text, paraId );
+ } );
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabSelectionListener(
+ ( tabPane, oldTab, newTab ) -> {
+ if( newTab == null ) {
+ // Clear the preview pane when closing an editor. When the last
+ // tab is closed, this ensures that the preview pane is empty.
+ getPreviewPane().clear();
+ }
+ else {
+ final var tab = (FileEditorTab) newTab;
+ updateVariableNameInjector( tab );
+ process( tab );
+ }
+ }
+ );
+ }
+
+ /**
+ * Reloads the preferences from the previous session.
+ */
+ private void initPreferences() {
+ initDefinitionPane();
+ getFileEditorPane().initPreferences();
+ getUserPreferences().addSaveEventHandler( mRPreferencesListener );
+ }
+
+ private void initVariableNameInjector() {
+ updateVariableNameInjector( getActiveFileEditorTab() );
+ }
+
+ /**
+ * Calls the listener when the given node is shown for the first time. The
+ * visible property is not the same as the initial showing event; visibility
+ * can be triggered numerous times (such as going off screen).
+ * <p>
+ * This is called, for example, before the drag handler can be attached,
+ * because the scrollbar for the text editor pane must be visible.
+ * </p>
+ *
+ * @param node The node to watch for showing.
+ * @param consumer The consumer to invoke when the event fires.
+ */
+ private void addShowListener(
+ final Node node, final Consumer<Void> consumer ) {
+ final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
+ runLater( () -> {
+ if( newShow != null && newShow ) {
+ try {
+ consumer.accept( null );
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+ } );
+
+ Val.flatMap( node.sceneProperty(), Scene::windowProperty )
+ .flatMap( Window::showingProperty )
+ .addListener( listener );
+ }
+
+ private void scrollToParagraph( final int id ) {
+ scrollToParagraph( id, false );
+ }
+
+ /**
+ * @param id The paragraph to scroll to, will be approximated if it doesn't
+ * exist.
+ * @param force {@code true} means to force scrolling immediately, which
+ * should only be attempted when it is known that the document
+ * has been fully rendered. Otherwise the internal map of ID
+ * attributes will be incomplete and scrolling will flounder.
+ */
+ private void scrollToParagraph( final int id, final boolean force ) {
+ synchronized( mMutex ) {
+ final var previewPane = getPreviewPane();
+ final var scrollPane = previewPane.getScrollPane();
+ final int approxId = getActiveEditorPane().approximateParagraphId( id );
+
+ if( force ) {
+ previewPane.scrollTo( approxId );
+ }
+ else {
+ previewPane.tryScrollTo( approxId );
+ }
+
+ scrollPane.repaint();
+ }
+ }
+
+ private void updateVariableNameInjector( final FileEditorTab tab ) {
+ getDefinitionNameInjector().addListener( tab );
+ }
+
+ /**
+ * Called whenever the preview pane becomes out of sync with the file editor
+ * tab. This can be called when the text changes, the caret paragraph
+ * changes, or the file tab changes.
+ *
+ * @param tab The file editor tab that has been changed in some fashion.
+ */
+ private void process( final FileEditorTab tab ) {
+ if( tab != null ) {
+ getPreviewPane().setPath( tab.getPath() );
+
+ final Processor<String> processor = getProcessors().computeIfAbsent(
+ tab, p -> createProcessors( tab )
+ );
+
+ try {
+ processChain( processor, tab.getEditorText() );
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+ }
+
+ /**
+ * Executes the processing chain, operating on the given string.
+ *
+ * @param handler The first processor in the chain to call.
+ * @param text The initial value of the text to process.
+ * @return The final value of the text that was processed by the chain.
+ */
+ private String processChain( Processor<String> handler, String text ) {
+ while( handler != null && text != null ) {
+ text = handler.apply( text );
+ handler = handler.next();
+ }
+
+ return text;
+ }
+
+ private void renderActiveTab() {
+ process( getActiveFileEditorTab() );
+ }
+
+ /**
+ * Called when a definition source is opened.
+ *
+ * @param path Path to the definition source that was opened.
+ */
+ private void openDefinitions( final Path path ) {
+ try {
+ final var ds = createDefinitionSource( path );
+ setDefinitionSource( ds );
+
+ final var prefs = getUserPreferences();
+ prefs.definitionPathProperty().setValue( path.toFile() );
+ prefs.save();
+
+ final var tooltipPath = new Tooltip( path.toString() );
+ tooltipPath.setShowDelay( Duration.millis( 200 ) );
+
+ final var pane = getDefinitionPane();
+ pane.update( ds );
+ pane.addTreeChangeHandler( mTreeHandler );
+ pane.addKeyEventHandler( mDefinitionKeyHandler );
+ pane.filenameProperty().setValue( path.getFileName().toString() );
+ pane.setTooltip( tooltipPath );
+
+ interpolateResolvedMap();
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+
+ private void exportDefinitions( final Path path ) {
+ try {
+ final var pane = getDefinitionPane();
+ final var root = pane.getTreeView().getRoot();
+ final var problemChild = pane.isTreeWellFormed();
+
+ if( problemChild == null ) {
+ getDefinitionSource().getTreeAdapter().export( root, path );
+ }
+ else {
+ alert( "yaml.error.tree.form", problemChild.getValue() );
+ }
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+
+ private void interpolateResolvedMap() {
+ final var treeMap = getDefinitionPane().toMap();
+ final var map = new HashMap<>( treeMap );
+ MapInterpolator.interpolate( map );
+
+ getResolvedMap().clear();
+ getResolvedMap().putAll( map );
+ }
+
+ private void initDefinitionPane() {
+ openDefinitions( getDefinitionPath() );
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ /**
+ * Called when an {@link Observable} instance has changed. This is called
+ * by both the {@link Snitch} service and the notify service. The @link
+ * Snitch} service can be called for different file types, including
+ * {@link DefinitionSource} instances.
+ *
+ * @param observable The observed instance.
+ * @param value The noteworthy item.
+ */
+ @Override
+ public void update( final Observable observable, final Object value ) {
+ if( value instanceof Path && observable instanceof Snitch ) {
+ updateSelectedTab();
+ }
+ }
+
+ /**
+ * Called when a file has been modified.
+ */
+ private void updateSelectedTab() {
+ rerender();
+ }
+
+ /**
+ * After resetting the processors, they will refresh anew to be up-to-date
+ * with the files (text and definition) currently loaded into the editor.
+ */
+ private void resetProcessors() {
+ getProcessors().clear();
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
+ }
+
+ /**
+ * TODO: Upon closing, first remove the tab change listeners. (There's no
+ * need to re-render each tab when all are being closed.)
+ */
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditorTab() );
+ }
+
+ private void fileSaveAs() {
+ final FileEditorTab editor = getActiveFileEditorTab();
+ getFileEditorPane().saveEditorAs( editor );
+ getProcessors().remove( editor );
+
+ try {
+ process( editor );
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Edit actions -------------------------------------------------------
+
+ /**
+ * Transform the Markdown into HTML then copy that HTML into the copy
+ * buffer.
+ */
+ private void copyHtml() {
+ final var markdown = getActiveEditorPane().getText();
+ final var processors = createProcessorFactory().createProcessors(
+ getActiveFileEditorTab()
+ );
+
+ final var chain = processors.remove( HtmlPreviewProcessor.class );
+
+ final String html = processChain( chain, markdown );
+
+ final Clipboard clipboard = Clipboard.getSystemClipboard();
+ final ClipboardContent content = new ClipboardContent();
+ content.putString( html );
+ clipboard.setContent( content );
+ }
+
+ /**
+ * Used to find text in the active file editor window.
+ */
+ private void editFind() {
+ final TextField input = getFindTextField();
+ getStatusBar().setGraphic( input );
+ input.requestFocus();
+ }
+
+ public void editFindNext() {
+ getActiveFileEditorTab().searchNext( getFindTextField().getText() );
+ }
+
+ public void editPreferences() {
+ getUserPreferences().show();
+ }
+
+ //---- Insert actions -----------------------------------------------------
+
+ /**
+ * Delegates to the active editor to handle wrapping the current text
+ * selection with leading and trailing strings.
+ *
+ * @param leading The string to put before the selection.
+ * @param trailing The string to put after the selection.
+ */
+ private void insertMarkdown(
+ final String leading, final String trailing ) {
+ getActiveEditorPane().surroundSelection( leading, trailing );
+ }
+
+ private void insertMarkdown(
+ final String leading, final String trailing, final String hint ) {
+ getActiveEditorPane().surroundSelection( leading, trailing, hint );
+ }
+
+ //---- View actions -------------------------------------------------------
+
+ private void viewRefresh() {
+ rerender();
+ }
+
+ //---- Help actions -------------------------------------------------------
+
+ private void helpAbout() {
+ final Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( get( "Dialog.about.title" ) );
+ alert.setHeaderText( get( "Dialog.about.header" ) );
+ alert.setContentText( get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ //---- Member creators ----------------------------------------------------
+
+ private SpellChecker createSpellChecker() {
+ try {
+ final Collection<String> lexicon = readLexicon( "en.txt" );
+ return SymSpellSpeller.forLexicon( lexicon );
+ } catch( final Exception ex ) {
+ alert( ex );
+ return new PermissiveSpeller();
+ }
+ }
+
+ /**
+ * Factory to create processors that are suited to different file types.
+ *
+ * @param tab The tab that is subjected to processing.
+ * @return A processor suited to the file type specified by the tab's path.
+ */
+ private Processor<String> createProcessors( final FileEditorTab tab ) {
+ return createProcessorFactory().createProcessors( tab );
+ }
+
+ private ProcessorFactory createProcessorFactory() {
+ return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
+ }
+
+ private DefinitionPane createDefinitionPane() {
+ return new DefinitionPane();
+ }
+
+ private HTMLPreviewPane createHTMLPreviewPane() {
+ return new HTMLPreviewPane();
+ }
+
+ private DefinitionSource createDefaultDefinitionSource() {
+ return new YamlDefinitionSource( getDefinitionPath() );
+ }
+
+ private DefinitionSource createDefinitionSource( final Path path ) {
+ try {
+ return createDefinitionFactory().createDefinitionSource( path );
+ } catch( final Exception ex ) {
+ alert( ex );
+ return createDefaultDefinitionSource();
+ }
+ }
+
+ private TextField createFindTextField() {
+ return new TextField();
+ }
+
+ private DefinitionFactory createDefinitionFactory() {
+ return new DefinitionFactory();
+ }
+
+ private StatusBar createStatusBar() {
+ return new StatusBar();
+ }
+
+ private Scene createScene() {
+ final SplitPane splitPane = new SplitPane(
+ getDefinitionPane(),
+ getFileEditorPane(),
+ getPreviewPane() );
+
+ splitPane.setDividerPositions(
+ getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
+ getFloat( K_PANE_SPLIT_EDITOR, .60f ),
+ getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
+
+ getDefinitionPane().prefHeightProperty()
+ .bind( splitPane.heightProperty() );
+
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1280, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setBottom( getStatusBar() );
+ borderPane.setCenter( splitPane );
+
+ final VBox statusBar = new VBox();
+ statusBar.setAlignment( Pos.BASELINE_CENTER );
+ statusBar.getChildren().add( getLineNumberText() );
+ getStatusBar().getRightItems().add( statusBar );
+
+ // Force preview pane refresh on Windows.
+ if( SystemUtils.IS_OS_WINDOWS ) {
+ splitPane.getDividers().get( 1 ).positionProperty().addListener(
+ ( l, oValue, nValue ) -> runLater(
+ () -> getPreviewPane().getScrollPane().repaint()
+ )
+ );
+ }
+
+ return new Scene( borderPane );
+ }
+
+ private Text createLineNumberText() {
+ return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
+ }
+
+ private Node createMenuBar() {
+ final BooleanBinding activeFileEditorIsNull =
+ getFileEditorPane().activeFileEditorProperty().isNull();
+
+ // File actions
+ final Action fileNewAction = new ActionBuilder()
+ .setText( "Main.menu.file.new" )
+ .setAccelerator( "Shortcut+N" )
+ .setIcon( FILE_ALT )
+ .setAction( e -> fileNew() )
+ .build();
+ final Action fileOpenAction = new ActionBuilder()
+ .setText( "Main.menu.file.open" )
+ .setAccelerator( "Shortcut+O" )
+ .setIcon( FOLDER_OPEN_ALT )
+ .setAction( e -> fileOpen() )
+ .build();
+ final Action fileCloseAction = new ActionBuilder()
+ .setText( "Main.menu.file.close" )
+ .setAccelerator( "Shortcut+W" )
+ .setAction( e -> fileClose() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action fileCloseAllAction = new ActionBuilder()
+ .setText( "Main.menu.file.close_all" )
+ .setAction( e -> fileCloseAll() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action fileSaveAction = new ActionBuilder()
+ .setText( "Main.menu.file.save" )
+ .setAccelerator( "Shortcut+S" )
+ .setIcon( FLOPPY_ALT )
+ .setAction( e -> fileSave() )
+ .setDisable( createActiveBooleanProperty(
+ FileEditorTab::modifiedProperty ).not() )
+ .build();
+ final Action fileSaveAsAction = new ActionBuilder()
+ .setText( "Main.menu.file.save_as" )
+ .setAction( e -> fileSaveAs() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action fileSaveAllAction = new ActionBuilder()
+ .setText( "Main.menu.file.save_all" )
+ .setAccelerator( "Shortcut+Shift+S" )
+ .setAction( e -> fileSaveAll() )
+ .setDisable( Bindings.not(
+ getFileEditorPane().anyFileEditorModifiedProperty() ) )
+ .build();
+ final Action fileExitAction = new ActionBuilder()
+ .setText( "Main.menu.file.exit" )
+ .setAction( e -> fileExit() )
+ .build();
+
+ // Edit actions
+ final Action editCopyHtmlAction = new ActionBuilder()
+ .setText( "Main.menu.edit.copy.html" )
+ .setIcon( HTML5 )
+ .setAction( e -> copyHtml() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ final Action editUndoAction = new ActionBuilder()
+ .setText( "Main.menu.edit.undo" )
+ .setAccelerator( "Shortcut+Z" )
+ .setIcon( UNDO )
+ .setAction( e -> getActiveEditorPane().undo() )
+ .setDisable( createActiveBooleanProperty(
+ FileEditorTab::canUndoProperty ).not() )
+ .build();
+ final Action editRedoAction = new ActionBuilder()
+ .setText( "Main.menu.edit.redo" )
+ .setAccelerator( "Shortcut+Y" )
+ .setIcon( REPEAT )
+ .setAction( e -> getActiveEditorPane().redo() )
+ .setDisable( createActiveBooleanProperty(
+ FileEditorTab::canRedoProperty ).not() )
+ .build();
+
+ final Action editCutAction = new ActionBuilder()
+ .setText( "Main.menu.edit.cut" )
+ .setAccelerator( "Shortcut+X" )
+ .setIcon( CUT )
+ .setAction( e -> getActiveEditorPane().cut() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editCopyAction = new ActionBuilder()
+ .setText( "Main.menu.edit.copy" )
+ .setAccelerator( "Shortcut+C" )
+ .setIcon( COPY )
+ .setAction( e -> getActiveEditorPane().copy() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editPasteAction = new ActionBuilder()
+ .setText( "Main.menu.edit.paste" )
+ .setAccelerator( "Shortcut+V" )
+ .setIcon( PASTE )
+ .setAction( e -> getActiveEditorPane().paste() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editSelectAllAction = new ActionBuilder()
+ .setText( "Main.menu.edit.selectAll" )
+ .setAccelerator( "Shortcut+A" )
+ .setAction( e -> getActiveEditorPane().selectAll() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ final Action editFindAction = new ActionBuilder()
+ .setText( "Main.menu.edit.find" )
+ .setAccelerator( "Ctrl+F" )
+ .setIcon( SEARCH )
+ .setAction( e -> editFind() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editFindNextAction = new ActionBuilder()
+ .setText( "Main.menu.edit.find.next" )
+ .setAccelerator( "F3" )
+ .setIcon( null )
+ .setAction( e -> editFindNext() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editPreferencesAction = new ActionBuilder()
+ .setText( "Main.menu.edit.preferences" )
+ .setAccelerator( "Ctrl+Alt+S" )
+ .setAction( e -> editPreferences() )
+ .build();
+
+ // Format actions
+ final Action formatBoldAction = new ActionBuilder()
+ .setText( "Main.menu.format.bold" )
+ .setAccelerator( "Shortcut+B" )
+ .setIcon( BOLD )
+ .setAction( e -> insertMarkdown( "**", "**" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action formatItalicAction = new ActionBuilder()
+ .setText( "Main.menu.format.italic" )
+ .setAccelerator( "Shortcut+I" )
+ .setIcon( ITALIC )
+ .setAction( e -> insertMarkdown( "*", "*" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action formatSuperscriptAction = new ActionBuilder()
+ .setText( "Main.menu.format.superscript" )
+ .setAccelerator( "Shortcut+[" )
+ .setIcon( SUPERSCRIPT )
+ .setAction( e -> insertMarkdown( "^", "^" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action formatSubscriptAction = new ActionBuilder()
+ .setText( "Main.menu.format.subscript" )
+ .setAccelerator( "Shortcut+]" )
+ .setIcon( SUBSCRIPT )
+ .setAction( e -> insertMarkdown( "~", "~" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action formatStrikethroughAction = new ActionBuilder()
+ .setText( "Main.menu.format.strikethrough" )
+ .setAccelerator( "Shortcut+T" )
+ .setIcon( STRIKETHROUGH )
+ .setAction( e -> insertMarkdown( "~~", "~~" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ // Insert actions
+ final Action insertBlockquoteAction = new ActionBuilder()
+ .setText( "Main.menu.insert.blockquote" )
+ .setAccelerator( "Ctrl+Q" )
+ .setIcon( QUOTE_LEFT )
+ .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertCodeAction = new ActionBuilder()
+ .setText( "Main.menu.insert.code" )
+ .setAccelerator( "Shortcut+K" )
+ .setIcon( CODE )
+ .setAction( e -> insertMarkdown( "`", "`" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertFencedCodeBlockAction = new ActionBuilder()
+ .setText( "Main.menu.insert.fenced_code_block" )
+ .setAccelerator( "Shortcut+Shift+K" )
+ .setIcon( FILE_CODE_ALT )
+ .setAction( e -> insertMarkdown(
+ "\n\n```\n",
+ "\n```\n\n",
+ get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertLinkAction = new ActionBuilder()
+ .setText( "Main.menu.insert.link" )
+ .setAccelerator( "Shortcut+L" )
+ .setIcon( LINK )
+ .setAction( e -> getActiveEditorPane().insertLink() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertImageAction = new ActionBuilder()
+ .setText( "Main.menu.insert.image" )
+ .setAccelerator( "Shortcut+G" )
+ .setIcon( PICTURE_ALT )
+ .setAction( e -> getActiveEditorPane().insertImage() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ // Number of heading actions (H1 ... H3)
+ final int HEADINGS = 3;
+ final Action[] headings = new Action[ HEADINGS ];
+
+ for( int i = 1; i <= HEADINGS; i++ ) {
+ final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
+ final String markup = String.format( "%n%n%s ", hashes );
+ final String text = "Main.menu.insert.heading." + i;
+ final String accelerator = "Shortcut+" + i;
+ final String prompt = text + ".prompt";
+
+ headings[ i - 1 ] = new ActionBuilder()
+ .setText( text )
+ .setAccelerator( accelerator )
+ .setIcon( HEADER )
+ .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ }
+
+ final Action insertUnorderedListAction = new ActionBuilder()
+ .setText( "Main.menu.insert.unordered_list" )
+ .setAccelerator( "Shortcut+U" )
+ .setIcon( LIST_UL )
+ .setAction( e -> insertMarkdown( "\n\n* ", "" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertOrderedListAction = new ActionBuilder()
+ .setText( "Main.menu.insert.ordered_list" )
+ .setAccelerator( "Shortcut+Shift+O" )
+ .setIcon( LIST_OL )
+ .setAction( e -> insertMarkdown(
+ "\n\n1. ", "" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertHorizontalRuleAction = new ActionBuilder()
+ .setText( "Main.menu.insert.horizontal_rule" )
+ .setAccelerator( "Shortcut+H" )
+ .setAction( e -> insertMarkdown(
+ "\n\n---\n\n", "" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ // Definition actions
+ final Action definitionCreateAction = new ActionBuilder()
+ .setText( "Main.menu.definition.create" )
+ .setIcon( TREE )
+ .setAction( e -> getDefinitionPane().addItem() )
+ .build();
+ final Action definitionInsertAction = new ActionBuilder()
+ .setText( "Main.menu.definition.insert" )
+ .setAccelerator( "Ctrl+Space" )
+ .setIcon( STAR )
+ .setAction( e -> definitionInsert() )
+ .build();
+
+ // Help actions
+ final Action helpAboutAction = new ActionBuilder()
+ .setText( "Main.menu.help.about" )
+ .setAction( e -> helpAbout() )
+ .build();
+
+ //---- MenuBar ----
+
+ // File Menu
+ final var fileMenu = ActionUtils.createMenu(
+ get( "Main.menu.file" ),
+ fileNewAction,
+ fileOpenAction,
+ null,
+ fileCloseAction,
+ fileCloseAllAction,
+ null,
+ fileSaveAction,
+ fileSaveAsAction,
+ fileSaveAllAction,
+ null,
+ fileExitAction );
+
+ // Edit Menu
+ final var editMenu = ActionUtils.createMenu(
+ get( "Main.menu.edit" ),
+ editCopyHtmlAction,
+ null,
+ editUndoAction,
+ editRedoAction,
+ null,
+ editCutAction,
+ editCopyAction,
+ editPasteAction,
+ editSelectAllAction,
+ null,
+ editFindAction,
+ editFindNextAction,
+ null,
+ editPreferencesAction );
+
+ // Format Menu
+ final var formatMenu = ActionUtils.createMenu(
+ get( "Main.menu.format" ),
+ formatBoldAction,
+ formatItalicAction,
+ formatSuperscriptAction,
+ formatSubscriptAction,
+ formatStrikethroughAction
+ );
+
+ // Insert Menu
+ final var insertMenu = ActionUtils.createMenu(
+ get( "Main.menu.insert" ),
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headings[ 0 ],
+ headings[ 1 ],
+ headings[ 2 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction,
+ insertHorizontalRuleAction
+ );
+
+ // Definition Menu
+ final var definitionMenu = ActionUtils.createMenu(
+ get( "Main.menu.definition" ),
+ definitionCreateAction,
+ definitionInsertAction );
+
+ // Help Menu
+ final var helpMenu = ActionUtils.createMenu(
+ get( "Main.menu.help" ),
+ helpAboutAction );
+
+ //---- MenuBar ----
+ final var menuBar = new MenuBar(
+ fileMenu,
+ editMenu,
+ formatMenu,
+ insertMenu,
+ definitionMenu,
+ helpMenu );
+
+ //---- ToolBar ----
+ final var toolBar = ActionUtils.createToolBar(
+ fileNewAction,
+ fileOpenAction,
+ fileSaveAction,
+ null,
+ editUndoAction,
+ editRedoAction,
+ editCutAction,
+ editCopyAction,
+ editPasteAction,
+ null,
+ formatBoldAction,
+ formatItalicAction,
+ formatSuperscriptAction,
+ formatSubscriptAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headings[ 0 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction );
+
+ return new VBox( menuBar, toolBar );
+ }
+
+ /**
+ * Performs the autoinsert function on the active file editor.
+ */
+ private void definitionInsert() {
+ getDefinitionNameInjector().autoinsert();
+ }
+
+ /**
+ * Creates a boolean property that is bound to another boolean value of the
+ * active editor.
+ */
+ private BooleanProperty createActiveBooleanProperty(
+ final Function<FileEditorTab, ObservableBooleanValue> func ) {
+
+ final BooleanProperty b = new SimpleBooleanProperty();
+ final FileEditorTab tab = getActiveFileEditorTab();
+
+ if( tab != null ) {
+ b.bind( func.apply( tab ) );
+ }
+
+ getFileEditorPane().activeFileEditorProperty().addListener(
+ ( observable, oldFileEditor, newFileEditor ) -> {
+ b.unbind();
+
+ if( newFileEditor == null ) {
+ b.set( false );
+ }
+ else {
+ b.bind( func.apply( newFileEditor ) );
+ }
+ }
+ );
+
+ return b;
+ }
+
+ //---- Convenience accessors ----------------------------------------------
+
+ private Preferences getPreferences() {
+ return sOptions.getState();
+ }
+
+ private int getCurrentParagraphIndex() {
+ return getActiveEditorPane().getCurrentParagraphIndex();
+ }
+
+ private float getFloat( final String key, final float defaultValue ) {
+ return getPreferences().getFloat( key, defaultValue );
+ }
+
+ public Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ private MarkdownEditorPane getActiveEditorPane() {
+ return getActiveFileEditorTab().getEditorPane();
+ }
+
+ private FileEditorTab getActiveFileEditorTab() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ //---- Member accessors ---------------------------------------------------
+
+ protected Scene getScene() {
+ return mScene;
+ }
+
+ private SpellChecker getSpellChecker() {
+ return mSpellChecker;
+ }
+
+ private Map<FileEditorTab, Processor<String>> getProcessors() {
+ return mProcessors;
+ }
+
+ private FileEditorTabPane getFileEditorPane() {
+ return mFileEditorPane;
+ }
+
+ private HTMLPreviewPane getPreviewPane() {
+ return mPreviewPane;
+ }
+
+ private void setDefinitionSource(
+ final DefinitionSource definitionSource ) {
+ assert definitionSource != null;
+ mDefinitionSource = definitionSource;
+ }
+
+ private DefinitionSource getDefinitionSource() {
+ return mDefinitionSource;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ return mDefinitionPane;
+ }
+
+ private Text getLineNumberText() {
+ return mLineNumberText;
+ }
+
+ private StatusBar getStatusBar() {
+ return mStatusBar;
+ }
+
+ private TextField getFindTextField() {
+ return mFindTextField;
+ }
+
+ private DefinitionNameInjector getDefinitionNameInjector() {
+ return mDefinitionNameInjector;
+ }
+
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
+ private Map<String, String> getResolvedMap() {
+ return mResolvedMap;
+ }
+
+ //---- Persistence accessors ----------------------------------------------
+
+ private UserPreferences getUserPreferences() {
+ return UserPreferences.getInstance();
+ }
+
+ private Path getDefinitionPath() {
+ return getUserPreferences().getDefinitionPath();
+ }
+
+ //---- Spelling -----------------------------------------------------------
+
+ /**
+ * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}.
+ * This is called to spell check the document, rather than a single paragraph.
+ *
+ * @param text The full document text.
+ */
+ private void spellcheck(
+ final StyleClassedTextArea editor, final String text ) {
+ spellcheck( editor, text, -1 );
+ }
+
+ /**
+ * Spellchecks a subset of the entire document.
+ *
+ * @param text Look up words for this text in the lexicon.
+ * @param paraId Set to -1 to apply resulting style spans to the entire
+ * text.
+ */
+ private void spellcheck(
+ final StyleClassedTextArea editor, final String text, final int paraId ) {
+ final var builder = new StyleSpansBuilder<Collection<String>>();
+ final var runningIndex = new AtomicInteger( 0 );
+ final var checker = getSpellChecker();
+
+ // The text nodes must be relayed through a contextual "visitor" that
+ // can return text in chunks with correlative offsets into the string.
+ // This allows Markdown, R Markdown, XML, and R XML documents to return
+ // sets of words to check.
+
+ final var node = mParser.parse( text );
+ final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
+ // Treat hyphenated compound words as individual words.
+ final var check = visited.replace( '-', ' ' );
+
+ checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
+ prevIndex += bIndex;
+ currIndex += bIndex;
+
+ // Clear styling between lexiconically absent words.
+ builder.add( emptyList(), prevIndex - runningIndex.get() );
+ builder.add( singleton( "spelling" ), currIndex - prevIndex );
+ runningIndex.set( currIndex );
+ } );
+ } );
+
+ visitor.visit( node );
+
+ // If the running index was set, at least one word triggered the listener.
+ if( runningIndex.get() > 0 ) {
+ // Clear styling after the last lexiconically absent word.
+ builder.add( emptyList(), text.length() - runningIndex.get() );
+
+ final var spans = builder.create();
+
+ if( paraId >= 0 ) {
+ editor.setStyleSpans( paraId, 0, spans );
+ }
+ else {
+ editor.setStyleSpans( 0, spans );
+ }
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private Collection<String> readLexicon( final String filename )
+ throws Exception {
+ final var path = "/" + LEXICONS_DIRECTORY + "/" + filename;
+
+ try( final var resource = getClass().getResourceAsStream( path ) ) {
+ if( resource == null ) {
+ throw new FileNotFoundException( path );
+ }
+
+ try( final var isr = new InputStreamReader( resource, UTF_8 );
+ final var reader = new BufferedReader( isr ) ) {
+ return reader.lines().collect( Collectors.toList() );
+ }
+ }
+ }
+
+ // TODO: #59 -- Replace using Markdown processor instantiated for Markdown files.
+ private final Parser mParser = Parser.builder().build();
+
+ // TODO: #59 -- Replace with generic interface; provide Markdown/XML implementations.
+ private static final class TextVisitor {
+ private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
+ com.vladsch.flexmark.ast.Text.class, this::visit )
+ );
+
+ private final SpellCheckListener mConsumer;
+
+ public TextVisitor( final SpellCheckListener consumer ) {
+ mConsumer = consumer;
+ }
+
+ private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
+ if( node instanceof com.vladsch.flexmark.ast.Text ) {
+ mConsumer.accept( node.getChars().toString(),
+ node.getStartOffset(),
+ node.getEndOffset() );
+ }
+
+ mVisitor.visitChildren( node );
+ }
+ }
+}
src/main/java/com/keenwrite/Messages.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+import java.util.Stack;
+
+import static com.keenwrite.Constants.APP_BUNDLE_NAME;
+import static java.util.ResourceBundle.getBundle;
+
+/**
+ * Recursively resolves message properties. Property values can refer to other
+ * properties using a <code>${var}</code> syntax.
+ */
+public class Messages {
+
+ private static final ResourceBundle RESOURCE_BUNDLE =
+ getBundle( APP_BUNDLE_NAME );
+
+ private Messages() {
+ }
+
+ /**
+ * Return the value of a resource bundle value after having resolved any
+ * references to other bundle variables.
+ *
+ * @param props The bundle containing resolvable properties.
+ * @param s The value for a key to resolve.
+ * @return The value of the key with all references recursively dereferenced.
+ */
+ @SuppressWarnings("SameParameterValue")
+ private static String resolve( final ResourceBundle props, final String s ) {
+ final int len = s.length();
+ final Stack<StringBuilder> stack = new Stack<>();
+
+ StringBuilder sb = new StringBuilder( 256 );
+ boolean open = false;
+
+ for( int i = 0; i < len; i++ ) {
+ final char c = s.charAt( i );
+
+ switch( c ) {
+ case '$': {
+ if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
+ stack.push( sb );
+ sb = new StringBuilder( 256 );
+ i++;
+ open = true;
+ }
+
+ break;
+ }
+
+ case '}': {
+ if( open ) {
+ open = false;
+ final String name = sb.toString();
+
+ sb = stack.pop();
+ sb.append( props.getString( name ) );
+ break;
+ }
+ }
+
+ default: {
+ sb.append( c );
+ break;
+ }
+ }
+ }
+
+ if( open ) {
+ throw new IllegalArgumentException( "missing '}'" );
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Returns the value for a key from the message bundle.
+ *
+ * @param key Retrieve the value for this key.
+ * @return The value for the key.
+ */
+ public static String get( final String key ) {
+ try {
+ return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
+ } catch( final Exception ex ) {
+ return key;
+ }
+ }
+
+ public static String getLiteral( final String key ) {
+ return RESOURCE_BUNDLE.getString( key );
+ }
+
+ public static String get( final String key, final boolean interpolate ) {
+ return interpolate ? get( key ) : getLiteral( key );
+ }
+
+ /**
+ * Returns the value for a key from the message bundle with the arguments
+ * replacing <code>{#}</code> place holders.
+ *
+ * @param key Retrieve the value for this key.
+ * @param args The values to substitute for place holders.
+ * @return The value for the key.
+ */
+ public static String get( final String key, final Object... args ) {
+ return MessageFormat.format( get( key ), args );
+ }
+}
src/main/java/com/keenwrite/ScrollEventHandler.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.ScrollBar;
+import javafx.scene.control.skin.ScrollBarSkin;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.layout.StackPane;
+import org.fxmisc.flowless.VirtualizedScrollPane;
+import org.fxmisc.richtext.StyleClassedTextArea;
+
+import javax.swing.*;
+
+import static javafx.geometry.Orientation.VERTICAL;
+
+/**
+ * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to
+ * an instance of {@link JScrollBar}.
+ * <p>
+ * Called to synchronize the scrolling areas for either scrolling with the
+ * mouse or scrolling using the scrollbar's thumb. Both are required to avoid
+ * scrolling on the estimatedScrollYProperty that occurs when text events
+ * fire. Scrolling performed for text events are handled separately to ensure
+ * the preview panel scrolls to the same position in the Markdown editor,
+ * taking into account things like images, tables, and other potentially
+ * long vertical presentation items.
+ * </p>
+ */
+public final class ScrollEventHandler implements EventHandler<Event> {
+
+ private final class MouseHandler implements EventHandler<MouseEvent> {
+ private final EventHandler<? super MouseEvent> mOldHandler;
+
+ /**
+ * Constructs a new handler for mouse scrolling events.
+ *
+ * @param oldHandler Receives the event after scrolling takes place.
+ */
+ private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) {
+ mOldHandler = oldHandler;
+ }
+
+ @Override
+ public void handle( final MouseEvent event ) {
+ ScrollEventHandler.this.handle( event );
+ mOldHandler.handle( event );
+ }
+ }
+
+ private final class ScrollHandler implements EventHandler<ScrollEvent> {
+ @Override
+ public void handle( final ScrollEvent event ) {
+ ScrollEventHandler.this.handle( event );
+ }
+ }
+
+ private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane;
+ private final JScrollBar mPreviewScrollBar;
+ private final BooleanProperty mEnabled = new SimpleBooleanProperty();
+
+ /**
+ * @param editorScrollPane Scroll event source (human movement).
+ * @param previewScrollBar Scroll event destination (corresponding movement).
+ */
+ public ScrollEventHandler(
+ final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane,
+ final JScrollBar previewScrollBar ) {
+ mEditorScrollPane = editorScrollPane;
+ mPreviewScrollBar = previewScrollBar;
+
+ mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
+
+ final var thumb = getVerticalScrollBarThumb( mEditorScrollPane );
+ thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) );
+ }
+
+ /**
+ * Gets a property intended to be bound to selected property of the tab being
+ * scrolled. This is required because there's only one preview pane but
+ * multiple editor panes. Each editor pane maintains its own scroll position.
+ *
+ * @return A {@link BooleanProperty} representing whether the scroll
+ * events for this tab are to be executed.
+ */
+ public BooleanProperty enabledProperty() {
+ return mEnabled;
+ }
+
+ /**
+ * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm
+ * is based on Karl Tauber's ratio calculation.
+ *
+ * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent}
+ */
+ @Override
+ public void handle( final Event event ) {
+ if( isEnabled() ) {
+ final var eScrollPane = getEditorScrollPane();
+ final int eScrollY =
+ eScrollPane.estimatedScrollYProperty().getValue().intValue();
+ final int eHeight = (int)
+ (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
+ - eScrollPane.getHeight());
+ final double eRatio = eHeight > 0
+ ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
+
+ final var pScrollBar = getPreviewScrollBar();
+ final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
+ final var pScrollY = (int) (pHeight * eRatio);
+
+ pScrollBar.setValue( pScrollY );
+ pScrollBar.getParent().repaint();
+ }
+ }
+
+ private StackPane getVerticalScrollBarThumb(
+ final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
+ final ScrollBar scrollBar = getVerticalScrollBar( pane );
+ final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get());
+
+ for( final Node node : skin.getChildren() ) {
+ // Brittle, but what can you do?
+ if( node.getStyleClass().contains( "thumb" ) ) {
+ return (StackPane) node;
+ }
+ }
+
+ throw new IllegalArgumentException( "No scroll bar skin found." );
+ }
+
+ private ScrollBar getVerticalScrollBar(
+ final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
+
+ for( final Node node : pane.getChildrenUnmodifiable() ) {
+ if( node instanceof ScrollBar ) {
+ final ScrollBar scrollBar = (ScrollBar) node;
+
+ if( scrollBar.getOrientation() == VERTICAL ) {
+ return scrollBar;
+ }
+ }
+ }
+
+ throw new IllegalArgumentException( "No vertical scroll pane found." );
+ }
+
+ private boolean isEnabled() {
+ // TODO: As a minor optimization, when this is set to false, it could remove
+ // the MouseHandler and ScrollHandler so that events only dispatch to one
+ // object (instead of one per editor tab).
+ return mEnabled.get();
+ }
+
+ private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() {
+ return mEditorScrollPane;
+ }
+
+ private JScrollBar getPreviewScrollBar() {
+ return mPreviewScrollBar;
+ }
+}
src/main/java/com/keenwrite/Services.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+/**
+ * Responsible for loading services. The services are treated as singleton
+ * instances.
+ */
+public class Services {
+
+ @SuppressWarnings("rawtypes")
+ private static final Map<Class, Object> SINGLETONS = new HashMap<>();
+
+ /**
+ * Loads a service based on its interface definition. This will return an
+ * existing instance if the class has already been instantiated.
+ *
+ * @param <T> The service to load.
+ * @param api The interface definition for the service.
+ * @return A class that implements the interface.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T load( final Class<T> api ) {
+ final T o = (T) get( api );
+
+ return o == null ? newInstance( api ) : o;
+ }
+
+ private static <T> T newInstance( final Class<T> api ) {
+ final ServiceLoader<T> services = ServiceLoader.load( api );
+
+ for( final T service : services ) {
+ if( service != null ) {
+ // Re-use the same instance the next time the class is loaded.
+ put( api, service );
+ return service;
+ }
+ }
+
+ throw new RuntimeException( "No implementation for: " + api );
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static void put( final Class key, Object value ) {
+ SINGLETONS.put( key, value );
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static Object get( final Class api ) {
+ return SINGLETONS.get( api );
+ }
+}
src/main/java/com/keenwrite/StatusBarNotifier.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite;
+
+import com.keenwrite.service.events.Notifier;
+import org.controlsfx.control.StatusBar;
+
+import static com.keenwrite.Constants.STATUS_BAR_OK;
+import static com.keenwrite.Messages.get;
+import static javafx.application.Platform.runLater;
+
+/**
+ * Responsible for passing notifications about exceptions (or other error
+ * messages) through the application. Once the Event Bus is implemented, this
+ * class can go away.
+ */
+public class StatusBarNotifier {
+ private static final String OK = get( STATUS_BAR_OK, "OK" );
+
+ private static final Notifier sNotifier = Services.load( Notifier.class );
+ private static StatusBar sStatusBar;
+
+ public static void setStatusBar( final StatusBar statusBar ) {
+ sStatusBar = statusBar;
+ }
+
+ /**
+ * Resets the status bar to a default message.
+ */
+ public static void clearAlert() {
+ // Don't burden the repaint thread if there's no status bar change.
+ if( !OK.equals( sStatusBar.getText() ) ) {
+ update( OK );
+ }
+ }
+
+ /**
+ * Updates the status bar with a custom message.
+ *
+ * @param key The resource bundle key associated with a message (typically
+ * to inform the user about an error).
+ */
+ public static void alert( final String key ) {
+ update( get( key ) );
+ }
+
+ /**
+ * Updates the status bar with a custom message.
+ *
+ * @param key The property key having a value to populate with arguments.
+ * @param args The placeholder values to substitute into the key's value.
+ */
+ public static void alert( final String key, final Object... args ) {
+ update( get( key, args ) );
+ }
+
+ /**
+ * Called when an exception occurs that warrants the user's attention.
+ *
+ * @param t The exception with a message that the user should know about.
+ */
+ public static void alert( final Throwable t ) {
+ update( t.getMessage() );
+ }
+
+ /**
+ * Updates the status bar to show the first line of the given message.
+ *
+ * @param message The message to show in the status bar.
+ */
+ private static void update( final String message ) {
+ runLater(
+ () -> {
+ final var s = message == null ? "" : message;
+ final var i = s.indexOf( '\n' );
+ sStatusBar.setText( s.substring( 0, i > 0 ? i : s.length() ) );
+ }
+ );
+ }
+
+ /**
+ * Returns the global {@link Notifier} instance that can be used for opening
+ * pop-up alert messages.
+ *
+ * @return The pop-up {@link Notifier} dispatcher.
+ */
+ public static Notifier getNotifier() {
+ return sNotifier;
+ }
+}
src/main/java/com/keenwrite/adapters/DocumentAdapter.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.adapters;
+
+import org.xhtmlrenderer.event.DocumentListener;
+
+import static com.keenwrite.StatusBarNotifier.alert;
+
+/**
+ * Allows subclasses to implement specific events.
+ */
+public class DocumentAdapter implements DocumentListener {
+ @Override
+ public void documentStarted() {
+ }
+
+ @Override
+ public void documentLoaded() {
+ }
+
+ @Override
+ public void onLayoutException( final Throwable t ) {
+ alert( t );
+ }
+
+ @Override
+ public void onRenderException( final Throwable t ) {
+ alert( t );
+ }
+}
src/main/java/com/keenwrite/adapters/ReplacedElementAdapter.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.adapters;
+
+import org.w3c.dom.Element;
+import org.xhtmlrenderer.extend.ReplacedElementFactory;
+import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
+
+public abstract class ReplacedElementAdapter implements ReplacedElementFactory {
+ @Override
+ public void reset() {
+ }
+
+ @Override
+ public void remove( final Element e ) {
+ }
+
+ @Override
+ public void setFormSubmissionListener(
+ final FormSubmissionListener listener ) {
+ }
+}
src/main/java/com/keenwrite/controls/BrowseFileButton.java
+/*
+ * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.keenwrite.controls;
+
+import com.keenwrite.Messages;
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
+import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.event.ActionEvent;
+import javafx.scene.control.Button;
+import javafx.scene.control.Tooltip;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.stage.FileChooser;
+import javafx.stage.FileChooser.ExtensionFilter;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Button that opens a file chooser to select a local file for a URL.
+ */
+public class BrowseFileButton extends Button {
+ private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
+
+ public BrowseFileButton() {
+ setGraphic(
+ FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT )
+ );
+ setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
+ setOnAction( this::browse );
+
+ disableProperty().bind( basePath.isNull() );
+
+ // workaround for a JavaFX bug:
+ // avoid closing the dialog that contains this control when the user
+ // closes the FileChooser or DirectoryChooser using the ESC key
+ addEventHandler( KeyEvent.KEY_RELEASED, e -> {
+ if( e.getCode() == KeyCode.ESCAPE ) {
+ e.consume();
+ }
+ } );
+ }
+
+ public void addExtensionFilter( ExtensionFilter extensionFilter ) {
+ extensionFilters.add( extensionFilter );
+ }
+
+ // 'basePath' property
+ private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
+
+ public Path getBasePath() {
+ return basePath.get();
+ }
+
+ public void setBasePath( Path basePath ) {
+ this.basePath.set( basePath );
+ }
+
+ // 'url' property
+ private final ObjectProperty<String> url = new SimpleObjectProperty<>();
+
+ public ObjectProperty<String> urlProperty() {
+ return url;
+ }
+
+ protected void browse( ActionEvent e ) {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
+ fileChooser.getExtensionFilters().addAll( extensionFilters );
+ fileChooser.getExtensionFilters()
+ .add( new ExtensionFilter( Messages.get(
+ "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
+ fileChooser.setInitialDirectory( getInitialDirectory() );
+ File result = fileChooser.showOpenDialog( getScene().getWindow() );
+ if( result != null ) {
+ updateUrl( result );
+ }
+ }
+
+ protected File getInitialDirectory() {
+ //TODO build initial directory based on current value of 'url' property
+ return getBasePath().toFile();
+ }
+
+ protected void updateUrl( File file ) {
+ String newUrl;
+ try {
+ newUrl = getBasePath().relativize( file.toPath() ).toString();
+ } catch( IllegalArgumentException ex ) {
+ newUrl = file.toString();
+ }
+ url.set( newUrl.replace( '\\', '/' ) );
+ }
+}
src/main/java/com/keenwrite/controls/EscapeTextField.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.keenwrite.controls;
+
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.scene.control.TextField;
+import javafx.util.StringConverter;
+
+/**
+ * Responsible for escaping/unescaping characters for markdown.
+ */
+public class EscapeTextField extends TextField {
+
+ public EscapeTextField() {
+ escapedText.bindBidirectional(
+ textProperty(),
+ new StringConverter<>() {
+ @Override
+ public String toString( String object ) {
+ return escape( object );
+ }
+
+ @Override
+ public String fromString( String string ) {
+ return unescape( string );
+ }
+ }
+ );
+ escapeCharacters.addListener(
+ e -> escapedText.set( escape( textProperty().get() ) )
+ );
+ }
+
+ // 'escapedText' property
+ private final StringProperty escapedText = new SimpleStringProperty();
+
+ public StringProperty escapedTextProperty() {
+ return escapedText;
+ }
+
+ // 'escapeCharacters' property
+ private final StringProperty escapeCharacters = new SimpleStringProperty();
+
+ public String getEscapeCharacters() {
+ return escapeCharacters.get();
+ }
+
+ public void setEscapeCharacters( String escapeCharacters ) {
+ this.escapeCharacters.set( escapeCharacters );
+ }
+
+ private String escape( final String s ) {
+ final String escapeChars = getEscapeCharacters();
+
+ return isEmpty( escapeChars ) ? s :
+ s.replaceAll( "([" + escapeChars.replaceAll(
+ "(.)",
+ "\\\\$1" ) + "])", "\\\\$1" );
+ }
+
+ private String unescape( final String s ) {
+ final String escapeChars = getEscapeCharacters();
+
+ return isEmpty( escapeChars ) ? s :
+ s.replaceAll( "\\\\([" + escapeChars
+ .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
+ }
+
+ private static boolean isEmpty( final String s ) {
+ return s == null || s.isEmpty();
+ }
+}
src/main/java/com/keenwrite/definition/DefinitionFactory.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+import com.keenwrite.AbstractFileFactory;
+import com.keenwrite.FileType;
+import com.keenwrite.definition.yaml.YamlDefinitionSource;
+
+import java.nio.file.Path;
+
+import static com.keenwrite.Constants.GLOB_PREFIX_DEFINITION;
+import static com.keenwrite.FileType.YAML;
+import static com.keenwrite.util.ProtocolResolver.getProtocol;
+
+/**
+ * Responsible for creating objects that can read and write definition data
+ * sources. The data source could be YAML, TOML, JSON, flat files, or from a
+ * database.
+ */
+public class DefinitionFactory extends AbstractFileFactory {
+
+ /**
+ * Default (empty) constructor.
+ */
+ public DefinitionFactory() {
+ }
+
+ /**
+ * Creates a definition source capable of reading definitions from the given
+ * path.
+ *
+ * @param path Path to a resource containing definitions.
+ * @return The definition source appropriate for the given path.
+ */
+ public DefinitionSource createDefinitionSource( final Path path ) {
+ assert path != null;
+
+ final var protocol = getProtocol( path.toString() );
+ DefinitionSource result = null;
+
+ if( protocol.isFile() ) {
+ final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION );
+ result = createFileDefinitionSource( filetype, path );
+ }
+ else {
+ unknownFileType( protocol, path.toString() );
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates a definition source based on the file type.
+ *
+ * @param filetype Property key name suffix from settings.properties file.
+ * @param path Path to the file that corresponds to the extension.
+ * @return A DefinitionSource capable of parsing the data stored at the path.
+ */
+ private DefinitionSource createFileDefinitionSource(
+ final FileType filetype, final Path path ) {
+ assert filetype != null;
+ assert path != null;
+
+ if( filetype == YAML ) {
+ return new YamlDefinitionSource( path );
+ }
+
+ throw new IllegalArgumentException( filetype.toString() );
+ }
+}
src/main/java/com/keenwrite/definition/DefinitionPane.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
+import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.util.StringConverter;
+
+import java.util.*;
+
+import static com.keenwrite.Messages.get;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
+import static javafx.geometry.Pos.CENTER;
+import static javafx.scene.input.KeyEvent.KEY_PRESSED;
+
+/**
+ * Provides the user interface that holds a {@link TreeView}, which
+ * allows users to interact with key/value pairs loaded from the
+ * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
+ */
+public final class DefinitionPane extends BorderPane {
+
+ /**
+ * Contains a view of the definitions.
+ */
+ private final TreeView<String> mTreeView = new TreeView<>();
+
+ /**
+ * Handlers for key press events.
+ */
+ private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
+ = new HashSet<>();
+
+ /**
+ * Definition file name shown in the title of the pane.
+ */
+ private final StringProperty mFilename = new SimpleStringProperty();
+
+ private final TitledPane mTitledPane = new TitledPane();
+
+ /**
+ * Constructs a definition pane with a given tree view root.
+ */
+ public DefinitionPane() {
+ final var treeView = getTreeView();
+ treeView.setEditable( true );
+ treeView.setCellFactory( cell -> createTreeCell() );
+ treeView.setContextMenu( createContextMenu() );
+ treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
+ treeView.setShowRoot( false );
+ getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
+
+ final var bCreate = createButton(
+ "create", TREE, e -> addItem() );
+ final var bRename = createButton(
+ "rename", EDIT, e -> editSelectedItem() );
+ final var bDelete = createButton(
+ "delete", TRASH, e -> deleteSelectedItems() );
+
+ final var buttonBar = new HBox();
+ buttonBar.getChildren().addAll( bCreate, bRename, bDelete );
+ buttonBar.setAlignment( CENTER );
+ buttonBar.setSpacing( 10 );
+
+ final var titledPane = getTitledPane();
+ titledPane.textProperty().bind( mFilename );
+ titledPane.setContent( treeView );
+ titledPane.setCollapsible( false );
+ titledPane.setPadding( new Insets( 0, 0, 0, 0 ) );
+
+ setTop( buttonBar );
+ setCenter( titledPane );
+ setAlignment( buttonBar, Pos.TOP_CENTER );
+ setAlignment( titledPane, Pos.TOP_CENTER );
+
+ titledPane.prefHeightProperty().bind( this.heightProperty() );
+ }
+
+ public void setTooltip( final Tooltip tooltip ) {
+ getTitledPane().setTooltip( tooltip );
+ }
+
+ private TitledPane getTitledPane() {
+ return mTitledPane;
+ }
+
+ private Button createButton(
+ final String msgKey,
+ final FontAwesomeIcon icon,
+ final EventHandler<ActionEvent> eventHandler ) {
+ final var keyPrefix = "Pane.definition.button." + msgKey;
+ final var button = new Button( get( keyPrefix + ".label" ) );
+ button.setOnAction( eventHandler );
+
+ button.setGraphic(
+ FontAwesomeIconFactory.get().createIcon( icon )
+ );
+ button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
+
+ return button;
+ }
+
+ /**
+ * Changes the root of the {@link TreeView} to the root of the
+ * {@link TreeView} from the {@link DefinitionSource}.
+ *
+ * @param definitionSource Container for the hierarchy of key/value pairs
+ * to replace the existing hierarchy.
+ */
+ public void update( final DefinitionSource definitionSource ) {
+ assert definitionSource != null;
+
+ final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
+ final TreeItem<String> root = treeAdapter.adapt(
+ get( "Pane.definition.node.root.title" )
+ );
+
+ getTreeView().setRoot( root );
+ }
+
+ public Map<String, String> toMap() {
+ return TreeItemAdapter.toMap( getTreeView().getRoot() );
+ }
+
+ /**
+ * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
+ * is modified. The modifications include: item value changes, item additions,
+ * and item removals.
+ * <p>
+ * Safe to call multiple times; if a handler is already registered, the
+ * old handler is used.
+ * </p>
+ *
+ * @param handler The handler to call whenever any {@link TreeItem} changes.
+ */
+ public void addTreeChangeHandler(
+ final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
+ final TreeItem<String> root = getTreeView().getRoot();
+ root.addEventHandler( TreeItem.valueChangedEvent(), handler );
+ root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
+ }
+
+ public void addKeyEventHandler(
+ final EventHandler<? super KeyEvent> handler ) {
+ getKeyEventHandlers().add( handler );
+ }
+
+ /**
+ * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
+ * well-formed for export. A tree is considered well-formed if the following
+ * conditions are met:
+ *
+ * <ul>
+ * <li>The root node contains at least one child node having a leaf.</li>
+ * <li>There are no leaf nodes with sibling leaf nodes.</li>
+ * </ul>
+ *
+ * @return {@code null} if the document is well-formed, otherwise the
+ * problematic child {@link TreeItem}.
+ */
+ public TreeItem<String> isTreeWellFormed() {
+ final var root = getTreeView().getRoot();
+
+ for( final var child : root.getChildren() ) {
+ final var problemChild = isWellFormed( child );
+
+ if( child.isLeaf() || problemChild != null ) {
+ return problemChild;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Determines whether the document is well-formed by ensuring that
+ * child branches do not contain multiple leaves.
+ *
+ * @param item The sub-tree to check for well-formedness.
+ * @return {@code null} when the tree is well-formed, otherwise the
+ * problematic {@link TreeItem}.
+ */
+ private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
+ int childLeafs = 0;
+ int childBranches = 0;
+
+ for( final TreeItem<String> child : item.getChildren() ) {
+ if( child.isLeaf() ) {
+ childLeafs++;
+ }
+ else {
+ childBranches++;
+ }
+
+ final var problemChild = isWellFormed( child );
+
+ if( problemChild != null ) {
+ return problemChild;
+ }
+ }
+
+ return ((childBranches > 0 && childLeafs == 0) ||
+ (childBranches == 0 && childLeafs <= 1)) ? null : item;
+ }
+
+ /**
+ * Delegates to {@link DefinitionTreeItem#findLeafExact(String)}.
+ *
+ * @param text The value to find, never {@code null}.
+ * @return The leaf that contains the given value, or {@code null} if
+ * not found.
+ */
+ public DefinitionTreeItem<String> findLeafExact( final String text ) {
+ return getTreeRoot().findLeafExact( text );
+ }
+
+ /**
+ * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
+ *
+ * @param text The value to find, never {@code null}.
+ * @return The leaf that contains the given value, or {@code null} if
+ * not found.
+ */
+ public DefinitionTreeItem<String> findLeafContains( final String text ) {
+ return getTreeRoot().findLeafContains( text );
+ }
+
+ /**
+ * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
+ *
+ * @param text The value to find, never {@code null}.
+ * @return The leaf that contains the given value, or {@code null} if
+ * not found.
+ */
+ public DefinitionTreeItem<String> findLeafContainsNoCase(
+ final String text ) {
+ return getTreeRoot().findLeafContainsNoCase( text );
+ }
+
+ /**
+ * Delegates to {@link DefinitionTreeItem#findLeafStartsWith(String)}.
+ *
+ * @param text The value to find, never {@code null}.
+ * @return The leaf that contains the given value, or {@code null} if
+ * not found.
+ */
+ public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
+ return getTreeRoot().findLeafStartsWith( text );
+ }
+
+ /**
+ * Expands the node to the root, recursively.
+ *
+ * @param <T> The type of tree item to expand (usually String).
+ * @param node The node to expand.
+ */
+ public <T> void expand( final TreeItem<T> node ) {
+ if( node != null ) {
+ expand( node.getParent() );
+
+ if( !node.isLeaf() ) {
+ node.setExpanded( true );
+ }
+ }
+ }
+
+ public void select( final TreeItem<String> item ) {
+ getSelectionModel().clearSelection();
+ getSelectionModel().select( getTreeView().getRow( item ) );
+ }
+
+ /**
+ * Collapses the tree, recursively.
+ */
+ public void collapse() {
+ collapse( getTreeRoot().getChildren() );
+ }
+
+ /**
+ * Collapses the tree, recursively.
+ *
+ * @param <T> The type of tree item to expand (usually String).
+ * @param nodes The nodes to collapse.
+ */
+ private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
+ for( final var node : nodes ) {
+ node.setExpanded( false );
+ collapse( node.getChildren() );
+ }
+ }
+
+ /**
+ * @return {@code true} when the user is editing a {@link TreeItem}.
+ */
+ private boolean isEditingTreeItem() {
+ return getTreeView().editingItemProperty().getValue() != null;
+ }
+
+ /**
+ * Changes to edit mode for the selected item.
+ */
+ private void editSelectedItem() {
+ getTreeView().edit( getSelectedItem() );
+ }
+
+ /**
+ * Removes all selected items from the {@link TreeView}.
+ */
+ private void deleteSelectedItems() {
+ for( final var item : getSelectedItems() ) {
+ final var parent = item.getParent();
+
+ if( parent != null ) {
+ parent.getChildren().remove( item );
+ }
+ }
+ }
+
+ /**
+ * Deletes the selected item.
+ */
+ private void deleteSelectedItem() {
+ final var c = getSelectedItem();
+ getSiblings( c ).remove( c );
+ }
+
+ /**
+ * Adds a new item under the selected item (or root if nothing is selected).
+ * There are a few conditions to consider: when adding to the root,
+ * when adding to a leaf, and when adding to a non-leaf. Items added to the
+ * root must contain two items: a key and a value.
+ */
+ public void addItem() {
+ final var value = createTreeItem();
+ getSelectedItem().getChildren().add( value );
+ expand( value );
+ select( value );
+ }
+
+ private ContextMenu createContextMenu() {
+ final ContextMenu menu = new ContextMenu();
+ final ObservableList<MenuItem> items = menu.getItems();
+
+ addMenuItem( items, "Definition.menu.create" )
+ .setOnAction( e -> addItem() );
+
+ addMenuItem( items, "Definition.menu.rename" )
+ .setOnAction( e -> editSelectedItem() );
+
+ addMenuItem( items, "Definition.menu.remove" )
+ .setOnAction( e -> deleteSelectedItem() );
+
+ return menu;
+ }
+
+ /**
+ * Executes hot-keys for edits to the definition tree.
+ *
+ * @param event Contains the key code of the key that was pressed.
+ */
+ private void keyEventFilter( final KeyEvent event ) {
+ if( !isEditingTreeItem() ) {
+ switch( event.getCode() ) {
+ case ENTER:
+ expand( getSelectedItem() );
+ event.consume();
+ break;
+
+ case DELETE:
+ deleteSelectedItems();
+ break;
+
+ case INSERT:
+ addItem();
+ break;
+
+ case R:
+ if( event.isControlDown() ) {
+ editSelectedItem();
+ }
+
+ break;
+ }
+
+ for( final var handler : getKeyEventHandlers() ) {
+ handler.handle( event );
+ }
+ }
+ }
+
+ /**
+ * Adds a menu item to a list of menu items.
+ *
+ * @param items The list of menu items to append to.
+ * @param labelKey The resource bundle key name for the menu item's label.
+ * @return The menu item added to the list of menu items.
+ */
+ private MenuItem addMenuItem(
+ final List<MenuItem> items, final String labelKey ) {
+ final MenuItem menuItem = createMenuItem( labelKey );
+ items.add( menuItem );
+ return menuItem;
+ }
+
+ private MenuItem createMenuItem( final String labelKey ) {
+ return new MenuItem( get( labelKey ) );
+ }
+
+ private DefinitionTreeItem<String> createTreeItem() {
+ return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
+ }
+
+ private TreeCell<String> createTreeCell() {
+ return new FocusAwareTextFieldTreeCell( createStringConverter() ) {
+ @Override
+ public void commitEdit( final String newValue ) {
+ super.commitEdit( newValue );
+ select( getTreeItem() );
+ requestFocus();
+ }
+ };
+ }
+
+ @Override
+ public void requestFocus() {
+ super.requestFocus();
+ getTreeView().requestFocus();
+ }
+
+ private StringConverter<String> createStringConverter() {
+ return new StringConverter<>() {
+ @Override
+ public String toString( final String object ) {
+ return object == null ? "" : object;
+ }
+
+ @Override
+ public String fromString( final String string ) {
+ return string == null ? "" : string;
+ }
+ };
+ }
+
+ /**
+ * Returns the tree view that contains the definition hierarchy.
+ *
+ * @return A non-null instance.
+ */
+ public TreeView<String> getTreeView() {
+ return mTreeView;
+ }
+
+ /**
+ * Returns this pane.
+ *
+ * @return this
+ */
+ public Node getNode() {
+ return this;
+ }
+
+ /**
+ * Returns the property used to set the title of the pane: the file name.
+ *
+ * @return A non-null property used for showing the definition file name.
+ */
+ public StringProperty filenameProperty() {
+ return mFilename;
+ }
+
+ /**
+ * Returns the root of the tree.
+ *
+ * @return The first node added to the definition tree.
+ */
+ private DefinitionTreeItem<String> getTreeRoot() {
+ final var root = getTreeView().getRoot();
+
+ return root instanceof DefinitionTreeItem
+ ? (DefinitionTreeItem<String>) root
+ : new DefinitionTreeItem<>( "root" );
+ }
+
+ private ObservableList<TreeItem<String>> getSiblings(
+ final TreeItem<String> item ) {
+ final var root = getTreeView().getRoot();
+ final var parent = (item == null || item == root) ? root : item.getParent();
+
+ return parent.getChildren();
+ }
+
+ private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
+ return getTreeView().getSelectionModel();
+ }
+
+ /**
+ * Returns a copy of all the selected items.
+ *
+ * @return A list, possibly empty, containing all selected items in the
+ * {@link TreeView}.
+ */
+ private List<TreeItem<String>> getSelectedItems() {
+ return new ArrayList<>( getSelectionModel().getSelectedItems() );
+ }
+
+ public TreeItem<String> getSelectedItem() {
+ final var item = getSelectionModel().getSelectedItem();
+ return item == null ? getTreeView().getRoot() : item;
+ }
+
+ private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
+ return mKeyEventHandlers;
+ }
+
+ /**
+ * Answers whether there are any definitions in the tree.
+ *
+ * @return {@code true} when there are no definitions; {@code false} when
+ * there's at least one definition.
+ */
+ public boolean isEmpty() {
+ return getTreeRoot().isEmpty();
+ }
+}
src/main/java/com/keenwrite/definition/DefinitionSource.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+/**
+ * Represents behaviours for reading and writing string definitions. This
+ * class cannot have any direct hooks into the user interface, as it defines
+ * entry points into the definition data model loaded into an object
+ * hierarchy. That hierarchy is converted to a UI model using an adapter
+ * pattern.
+ */
+public interface DefinitionSource {
+
+ /**
+ * Creates an object capable of producing view-based objects from this
+ * definition source.
+ *
+ * @return A hierarchical tree suitable for displaying in the definition pane.
+ */
+ TreeAdapter getTreeAdapter();
+}
src/main/java/com/keenwrite/definition/DefinitionTreeItem.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+import javafx.scene.control.TreeItem;
+
+import java.util.Stack;
+import java.util.function.BiFunction;
+
+import static java.text.Normalizer.Form.NFD;
+import static java.text.Normalizer.normalize;
+
+/**
+ * Provides behaviour afforded to definition keys and corresponding value.
+ *
+ * @param <T> The type of {@link TreeItem} (usually string).
+ */
+public class DefinitionTreeItem<T> extends TreeItem<T> {
+
+ /**
+ * Constructs a new item with a default value.
+ *
+ * @param value Passed up to superclass.
+ */
+ public DefinitionTreeItem( final T value ) {
+ super( value );
+ }
+
+ /**
+ * Finds a leaf starting at the current node with text that matches the given
+ * value. Search is performed case-sensitively.
+ *
+ * @param text The text to match against each leaf in the tree.
+ * @return The leaf that has a value exactly matching the given text.
+ */
+ public DefinitionTreeItem<T> findLeafExact( final String text ) {
+ return findLeaf( text, DefinitionTreeItem::valueEquals );
+ }
+
+ /**
+ * Finds a leaf starting at the current node with text that matches the given
+ * value. Search is performed case-sensitively.
+ *
+ * @param text The text to match against each leaf in the tree.
+ * @return The leaf that has a value that contains the given text.
+ */
+ public DefinitionTreeItem<T> findLeafContains( final String text ) {
+ return findLeaf( text, DefinitionTreeItem::valueContains );
+ }
+
+ /**
+ * Finds a leaf starting at the current node with text that matches the given
+ * value. Search is performed case-insensitively.
+ *
+ * @param text The text to match against each leaf in the tree.
+ * @return The leaf that has a value that contains the given text.
+ */
+ public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
+ return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
+ }
+
+ /**
+ * Finds a leaf starting at the current node with text that matches the given
+ * value. Search is performed case-sensitively.
+ *
+ * @param text The text to match against each leaf in the tree.
+ * @return The leaf that has a value that starts with the given text.
+ */
+ public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
+ return findLeaf( text, DefinitionTreeItem::valueStartsWith );
+ }
+
+ /**
+ * Finds a leaf starting at the current node with text that matches the given
+ * value.
+ *
+ * @param text The text to match against each leaf in the tree.
+ * @param findMode What algorithm is used to match the given text.
+ * @return The leaf that has a value starting with the given text, or {@code
+ * null} if there was no match found.
+ */
+ public DefinitionTreeItem<T> findLeaf(
+ final String text,
+ final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
+ final var stack = new Stack<DefinitionTreeItem<T>>();
+ stack.push( this );
+
+ // Don't hunt for blank (empty) keys.
+ boolean found = text.isBlank();
+
+ while( !found && !stack.isEmpty() ) {
+ final var node = stack.pop();
+
+ for( final var child : node.getChildren() ) {
+ final var result = (DefinitionTreeItem<T>) child;
+
+ if( result.isLeaf() ) {
+ if( found = findMode.apply( result, text ) ) {
+ return result;
+ }
+ }
+ else {
+ stack.push( result );
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the value of the string without diacritic marks.
+ *
+ * @return A non-null, possibly empty string.
+ */
+ private String getDiacriticlessValue() {
+ return normalize( getValue().toString(), NFD )
+ .replaceAll( "\\p{M}", "" );
+ }
+
+ /**
+ * Returns true if this node is a leaf and its value equals the given text.
+ *
+ * @param s The text to compare against the node value.
+ * @return true Node is a leaf and its value equals the given value.
+ */
+ private boolean valueEquals( final String s ) {
+ return isLeaf() && getValue().equals( s );
+ }
+
+ /**
+ * Returns true if this node is a leaf and its value contains the given text.
+ *
+ * @param s The text to compare against the node value.
+ * @return true Node is a leaf and its value contains the given value.
+ */
+ private boolean valueContains( final String s ) {
+ return isLeaf() && getDiacriticlessValue().contains( s );
+ }
+
+ /**
+ * Returns true if this node is a leaf and its value contains the given text.
+ *
+ * @param s The text to compare against the node value.
+ * @return true Node is a leaf and its value contains the given value.
+ */
+ private boolean valueContainsNoCase( final String s ) {
+ return isLeaf() && getDiacriticlessValue()
+ .toLowerCase().contains( s.toLowerCase() );
+ }
+
+ /**
+ * Returns true if this node is a leaf and its value starts with the given
+ * text.
+ *
+ * @param s The text to compare against the node value.
+ * @return true Node is a leaf and its value starts with the given value.
+ */
+ private boolean valueStartsWith( final String s ) {
+ return isLeaf() && getDiacriticlessValue().startsWith( s );
+ }
+
+ /**
+ * Returns the path for this node, with nodes made distinct using the
+ * separator character. This uses two loops: one for pushing nodes onto a
+ * stack and one for popping them off to create the path in desired order.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ public String toPath() {
+ return TreeItemAdapter.toPath( getParent() );
+ }
+
+ /**
+ * Answers whether there are any definitions in this tree.
+ *
+ * @return {@code true} when there are no definitions in the tree; {@code
+ * false} when there is at least one definition present.
+ */
+ public boolean isEmpty() {
+ return getChildren().isEmpty();
+ }
+}
src/main/java/com/keenwrite/definition/DocumentParser.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+/**
+ * Responsible for parsing structured document formats.
+ *
+ * @param <T> The type of "node" for the document's object model.
+ */
+public interface DocumentParser<T> {
+
+ /**
+ * Parses a document into a nested object hierarchy. The object returned
+ * from this call must be the root node in the document tree.
+ *
+ * @return The document's root node, which may be empty but never null.
+ */
+ T getDocumentRoot();
+}
src/main/java/com/keenwrite/definition/FocusAwareTextFieldTreeCell.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+import javafx.scene.Node;
+import javafx.scene.control.TextField;
+import javafx.scene.control.cell.TextFieldTreeCell;
+import javafx.util.StringConverter;
+
+/**
+ * Responsible for fixing a focus lost bug in the JavaFX implementation.
+ * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details.
+ * This implementation borrows from the official documentation on creating
+ * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm
+ */
+public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> {
+ private TextField mTextField;
+
+ public FocusAwareTextFieldTreeCell(
+ final StringConverter<String> converter ) {
+ super( converter );
+ }
+
+ @Override
+ public void startEdit() {
+ super.startEdit();
+ var textField = mTextField;
+
+ if( textField == null ) {
+ textField = createTextField();
+ }
+ else {
+ textField.setText( getItem() );
+ }
+
+ setText( null );
+ setGraphic( textField );
+ textField.selectAll();
+ textField.requestFocus();
+
+ // When the focus is lost, commit the edit then close the input field.
+ // This fixes the unexpected behaviour when user clicks away.
+ textField.focusedProperty().addListener( ( l, o, n ) -> {
+ if( !n ) {
+ commitEdit( mTextField.getText() );
+ }
+ } );
+
+ mTextField = textField;
+ }
+
+ @Override
+ public void cancelEdit() {
+ super.cancelEdit();
+ setText( getItem() );
+ setGraphic( getTreeItem().getGraphic() );
+ }
+
+ @Override
+ public void updateItem( String item, boolean empty ) {
+ super.updateItem( item, empty );
+
+ String text = null;
+ Node graphic = null;
+
+ if( !empty ) {
+ if( isEditing() ) {
+ final var textField = mTextField;
+
+ if( textField != null ) {
+ textField.setText( getString() );
+ }
+
+ graphic = textField;
+ }
+ else {
+ text = getString();
+ graphic = getTreeItem().getGraphic();
+ }
+ }
+
+ setText( text );
+ setGraphic( graphic );
+ }
+
+ private TextField createTextField() {
+ final var textField = new TextField( getString() );
+
+ textField.setOnKeyReleased( t -> {
+ switch( t.getCode() ) {
+ case ENTER -> commitEdit( textField.getText() );
+ case ESCAPE -> cancelEdit();
+ }
+ } );
+
+ return textField;
+ }
+
+ private String getString() {
+ return getConverter().toString( getItem() );
+ }
+}
src/main/java/com/keenwrite/definition/MapInterpolator.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+import com.keenwrite.sigils.YamlSigilOperator;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+
+import static com.keenwrite.sigils.YamlSigilOperator.REGEX_PATTERN;
+
+/**
+ * Responsible for performing string interpolation on key/value pairs stored
+ * in a map. The values in the map can use a delimited syntax to refer to
+ * keys in the map.
+ */
+public class MapInterpolator {
+ private static final int GROUP_DELIMITED = 1;
+
+ /**
+ * Empty.
+ */
+ private MapInterpolator() {
+ }
+
+ /**
+ * Performs string interpolation on the values in the given map. This will
+ * change any value in the map that contains a variable that matches
+ * {@link YamlSigilOperator#REGEX_PATTERN}.
+ *
+ * @param map Contains values that represent references to keys.
+ */
+ public static void interpolate( final Map<String, String> map ) {
+ map.replaceAll( ( k, v ) -> resolve( map, v ) );
+ }
+
+ /**
+ * Given a value with zero or more key references, this will resolve all
+ * the values, recursively. If a key cannot be dereferenced, the value will
+ * contain the key name.
+ *
+ * @param map Map to search for keys when resolving key references.
+ * @param value Value containing zero or more key references
+ * @return The given value with all embedded key references interpolated.
+ */
+ private static String resolve(
+ final Map<String, String> map, String value ) {
+ final Matcher matcher = REGEX_PATTERN.matcher( value );
+
+ while( matcher.find() ) {
+ final String keyName = matcher.group( GROUP_DELIMITED );
+ final String mapValue = map.get( keyName );
+ final String keyValue = mapValue == null
+ ? keyName
+ : resolve( map, mapValue );
+
+ value = value.replace( keyName, keyValue );
+ }
+
+ return value;
+ }
+}
src/main/java/com/keenwrite/definition/RootTreeItem.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+
+/**
+ * Indicates that this is the top-most {@link TreeItem}. This class allows
+ * the {@link TreeItemAdapter} to ignore the topmost definition. Such
+ * contortions are necessary because {@link TreeView} requires a root item
+ * that isn't part of the user's definition file.
+ * <p>
+ * Another approach would be to associate object pairs per {@link TreeItem},
+ * but that would be a waste of memory since the only "exception" case is
+ * the root {@link TreeItem}.
+ * </p>
+ *
+ * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
+ */
+public class RootTreeItem<T> extends DefinitionTreeItem<T> {
+ /**
+ * Default constructor, calls the superclass, no other behaviour.
+ *
+ * @param value The {@link TreeItem} node name to construct the superclass.
+ * @see TreeItemAdapter#toMap(TreeItem) for details on how this
+ * class is used.
+ */
+ public RootTreeItem( final T value ) {
+ super( value );
+ }
+}
src/main/java/com/keenwrite/definition/TreeAdapter.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+import javafx.scene.control.TreeItem;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+/**
+ * Responsible for converting an object hierarchy into a {@link TreeItem}
+ * hierarchy.
+ */
+public interface TreeAdapter {
+ /**
+ * Adapts the document produced by the given parser into a {@link TreeItem}
+ * object that can be presented to the user within a GUI.
+ *
+ * @param root The default root node name.
+ * @return The parsed document in a {@link TreeItem} that can be displayed
+ * in a panel.
+ */
+ TreeItem<String> adapt( String root );
+
+ /**
+ * Exports the given root node to the given path.
+ *
+ * @param root The root node to export.
+ * @param path Where to persist the data.
+ * @throws IOException Could not write the data to the given path.
+ */
+ void export( TreeItem<String> root, Path path ) throws IOException;
+}
src/main/java/com/keenwrite/definition/TreeItemAdapter.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.keenwrite.sigils.YamlSigilOperator;
+import com.keenwrite.preview.HTMLPreviewPane;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Stack;
+
+import static com.keenwrite.Constants.DEFAULT_MAP_SIZE;
+
+/**
+ * Given a {@link TreeItem}, this will generate a flat map with all the
+ * values in the tree recursively interpolated. The application integrates
+ * definition files as follows:
+ * <ol>
+ * <li>Load YAML file into {@link JsonNode} hierarchy.</li>
+ * <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
+ * <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
+ * <li>Substitute flat map variables into document as required.</li>
+ * </ol>
+ *
+ * <p>
+ * This class is responsible for producing the interpolated flat map. This
+ * allows dynamic edits of the {@link TreeView} to be displayed in the
+ * {@link HTMLPreviewPane} without having to reload the definition file.
+ * Reloading the definition file would work, but has a number of drawbacks.
+ * </p>
+ */
+public class TreeItemAdapter {
+ /**
+ * Separates YAML definition keys (e.g., the dots in {@code $root.node.var$}).
+ */
+ public static final String SEPARATOR = ".";
+
+ /**
+ * Default buffer length for keys ({@link StringBuilder} has 16 character
+ * buffer) that should be large enough for most keys to avoid reallocating
+ * memory to increase the {@link StringBuilder}'s buffer.
+ */
+ public static final int DEFAULT_KEY_LENGTH = 64;
+
+ /**
+ * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
+ * as a consecutive list.
+ */
+ private static final class TreeIterator
+ implements Iterator<TreeItem<String>> {
+ private final Stack<TreeItem<String>> mStack = new Stack<>();
+
+ public TreeIterator( final TreeItem<String> root ) {
+ if( root != null ) {
+ mStack.push( root );
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !mStack.isEmpty();
+ }
+
+ @Override
+ public TreeItem<String> next() {
+ final TreeItem<String> next = mStack.pop();
+ next.getChildren().forEach( mStack::push );
+
+ return next;
+ }
+ }
+
+ private TreeItemAdapter() {
+ }
+
+ /**
+ * Iterate over a given root node (at any level of the tree) and process each
+ * leaf node into a flat map. Values must be interpolated separately.
+ */
+ public static Map<String, String> toMap( final TreeItem<String> root ) {
+ final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
+ final TreeIterator iterator = new TreeIterator( root );
+
+ iterator.forEachRemaining( item -> {
+ if( item.isLeaf() ) {
+ map.put( toPath( item.getParent() ), item.getValue() );
+ }
+ } );
+
+ return map;
+ }
+
+
+ /**
+ * For a given node, this will ascend the tree to generate a key name
+ * that is associated with the leaf node's value.
+ *
+ * @param node Ascendants represent the key to this node's value.
+ * @param <T> Data type that the {@link TreeItem} contains.
+ * @return The string representation of the node's unique key.
+ */
+ public static <T> String toPath( TreeItem<T> node ) {
+ assert node != null;
+
+ final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH );
+ final Stack<TreeItem<T>> stack = new Stack<>();
+
+ while( node != null && !(node instanceof RootTreeItem) ) {
+ stack.push( node );
+ node = node.getParent();
+ }
+
+ // Gets set at end of first iteration (to avoid an if condition).
+ String separator = "";
+
+ while( !stack.empty() ) {
+ final T subkey = stack.pop().getValue();
+ key.append( separator );
+ key.append( subkey );
+ separator = SEPARATOR;
+ }
+
+ return YamlSigilOperator.entoken( key.toString() );
+ }
+}
src/main/java/com/keenwrite/definition/yaml/YamlDefinitionSource.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition.yaml;
+
+import com.keenwrite.definition.DefinitionSource;
+import com.keenwrite.definition.TreeAdapter;
+
+import java.nio.file.Path;
+
+/**
+ * Represents a definition data source for YAML files.
+ */
+public class YamlDefinitionSource implements DefinitionSource {
+
+ private final YamlTreeAdapter mYamlTreeAdapter;
+
+ /**
+ * Constructs a new YAML definition source, populated from the given file.
+ *
+ * @param path Path to the YAML definition file.
+ */
+ public YamlDefinitionSource( final Path path ) {
+ assert path != null;
+
+ mYamlTreeAdapter = new YamlTreeAdapter( path );
+ }
+
+ @Override
+ public TreeAdapter getTreeAdapter() {
+ return mYamlTreeAdapter;
+ }
+}
src/main/java/com/keenwrite/definition/yaml/YamlParser.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition.yaml;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.keenwrite.definition.DocumentParser;
+
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Responsible for reading a YAML document into an object hierarchy.
+ */
+public class YamlParser implements DocumentParser<JsonNode> {
+
+ /**
+ * Start of the Universe (the YAML document node that contains all others).
+ */
+ private final JsonNode mDocumentRoot;
+
+ /**
+ * Creates a new YamlParser instance that attempts to parse the contents
+ * of the YAML document given from a path. In the event that the file either
+ * does not exist or is empty, a fake
+ *
+ * @param path Path to a file containing YAML data to parse.
+ */
+ public YamlParser( final Path path ) {
+ assert path != null;
+ mDocumentRoot = parse( path );
+ }
+
+ /**
+ * Returns the parent node for the entire YAML document tree.
+ *
+ * @return The document root, never {@code null}.
+ */
+ @Override
+ public JsonNode getDocumentRoot() {
+ return mDocumentRoot;
+ }
+
+ /**
+ * Parses the given path containing YAML data into an object hierarchy.
+ *
+ * @param path {@link Path} to the YAML resource to parse.
+ * @return The parsed contents, or an empty object hierarchy.
+ */
+ private JsonNode parse( final Path path ) {
+ try( final InputStream in = Files.newInputStream( path ) ) {
+ return new ObjectMapper( new YAMLFactory() ).readTree( in );
+ } catch( final Exception e ) {
+ // Ensure that a document root node exists by relying on the
+ // default failure condition when processing. This is required
+ // because the input stream could not be read.
+ return new ObjectMapper().createObjectNode();
+ }
+ }
+}
src/main/java/com/keenwrite/definition/yaml/YamlTreeAdapter.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.definition.yaml;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import com.keenwrite.definition.RootTreeItem;
+import com.keenwrite.definition.TreeAdapter;
+import com.keenwrite.definition.DefinitionTreeItem;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Map.Entry;
+
+/**
+ * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
+ * interface and vice-versa.
+ */
+public class YamlTreeAdapter implements TreeAdapter {
+ private final YamlParser mParser;
+
+ /**
+ * Constructs a new instance that will use the given path to read
+ * the object hierarchy from a data source.
+ *
+ * @param path Path to YAML contents to parse.
+ */
+ public YamlTreeAdapter( final Path path ) {
+ mParser = new YamlParser( path );
+ }
+
+ @Override
+ public void export( final TreeItem<String> treeItem, final Path path )
+ throws IOException {
+ final YAMLMapper mapper = new YAMLMapper();
+ final ObjectNode root = mapper.createObjectNode();
+
+ // Iterate over the root item's children. The root item is used by the
+ // application to ensure definitions can always be added to a tree, as
+ // such it is not meant to be exported, only its children.
+ for( final TreeItem<String> child : treeItem.getChildren() ) {
+ export( child, root );
+ }
+
+ // Writes as UTF8 by default.
+ mapper.writeValue( path.toFile(), root );
+ }
+
+ /**
+ * Recursive method to generate an object hierarchy that represents the
+ * given {@link TreeItem} hierarchy.
+ *
+ * @param item The {@link TreeItem} to reproduce as an object hierarchy.
+ * @param node The {@link ObjectNode} to update to reflect the
+ * {@link TreeItem} hierarchy.
+ */
+ private void export( final TreeItem<String> item, ObjectNode node ) {
+ final var children = item.getChildren();
+
+ // If the current item has more than one non-leaf child, it's an
+ // object node and must become a new nested object.
+ if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
+ node = node.putObject( item.getValue() );
+ }
+
+ for( final TreeItem<String> child : children ) {
+ if( child.isLeaf() ) {
+ node.put( item.getValue(), child.getValue() );
+ }
+ else {
+ export( child, node );
+ }
+ }
+ }
+
+ /**
+ * Converts a YAML document to a {@link TreeItem} based on the document
+ * keys. Only the first document in the stream is adapted.
+ *
+ * @param root Root {@link TreeItem} node name.
+ * @return A {@link TreeItem} populated with all the keys in the YAML
+ * document.
+ */
+ public TreeItem<String> adapt( final String root ) {
+ final JsonNode rootNode = getYamlParser().getDocumentRoot();
+ final TreeItem<String> rootItem = createRootTreeItem( root );
+
+ rootItem.setExpanded( true );
+ adapt( rootNode, rootItem );
+ return rootItem;
+ }
+
+ /**
+ * Iterate over a given root node (at any level of the tree) and adapt each
+ * leaf node.
+ *
+ * @param rootNode A JSON node (YAML node) to adapt.
+ * @param rootItem The tree item to use as the root when processing the node.
+ */
+ private void adapt(
+ final JsonNode rootNode, final TreeItem<String> rootItem ) {
+ rootNode.fields().forEachRemaining(
+ ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
+ );
+ }
+
+ /**
+ * Recursively adapt each rootNode to a corresponding rootItem.
+ *
+ * @param rootNode The node to adapt.
+ * @param rootItem The item to adapt using the node's key.
+ */
+ private void adapt(
+ final Entry<String, JsonNode> rootNode,
+ final TreeItem<String> rootItem ) {
+ final JsonNode leafNode = rootNode.getValue();
+ final String key = rootNode.getKey();
+ final TreeItem<String> leaf = createTreeItem( key );
+
+ if( leafNode.isValueNode() ) {
+ leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
+ }
+
+ rootItem.getChildren().add( leaf );
+
+ if( leafNode.isObject() ) {
+ adapt( leafNode, leaf );
+ }
+ }
+
+ /**
+ * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
+ *
+ * @param value The node's value.
+ * @return A new {@link TreeItem}, never {@code null}.
+ */
+ private TreeItem<String> createTreeItem( final String value ) {
+ return new DefinitionTreeItem<>( value );
+ }
+
+ /**
+ * Creates a new {@link TreeItem} that is intended to be the root-level item
+ * added to the {@link TreeView}. This allows the root item to be
+ * distinguished from the other items so that reference keys do not include
+ * "Definition" as part of their name.
+ *
+ * @param value The node's value.
+ * @return A new {@link TreeItem}, never {@code null}.
+ */
+ private TreeItem<String> createRootTreeItem( final String value ) {
+ return new RootTreeItem<>( value );
+ }
+
+ public YamlParser getYamlParser() {
+ return mParser;
+ }
+}
src/main/java/com/keenwrite/dialogs/AbstractDialog.java
+/*
+ * Copyright 2017 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.dialogs;
+
+import static com.keenwrite.Messages.get;
+import com.keenwrite.service.events.impl.ButtonOrderPane;
+import static javafx.scene.control.ButtonType.CANCEL;
+import static javafx.scene.control.ButtonType.OK;
+import javafx.scene.control.Dialog;
+import javafx.stage.Window;
+
+/**
+ * Superclass that abstracts common behaviours for all dialogs.
+ *
+ * @param <T> The type of dialog to create (usually String).
+ */
+public abstract class AbstractDialog<T> extends Dialog<T> {
+
+ /**
+ * Ensures that all dialogs can be closed.
+ *
+ * @param owner The parent window of this dialog.
+ * @param title The messages title to display in the title bar.
+ */
+ @SuppressWarnings( "OverridableMethodCallInConstructor" )
+ public AbstractDialog( final Window owner, final String title ) {
+ setTitle( get( title ) );
+ setResizable( true );
+
+ initOwner( owner );
+ initCloseAction();
+ initDialogPane();
+ initDialogButtons();
+ initComponents();
+ }
+
+ /**
+ * Initialize the component layout.
+ */
+ protected abstract void initComponents();
+
+ /**
+ * Set the dialog to use a button order pane with an OK and a CANCEL button.
+ */
+ protected void initDialogPane() {
+ setDialogPane( new ButtonOrderPane() );
+ }
+
+ /**
+ * Set an OK and CANCEL button on the dialog.
+ */
+ protected void initDialogButtons() {
+ getDialogPane().getButtonTypes().addAll( OK, CANCEL );
+ }
+
+ /**
+ * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
+ * can always close the window, even if there's an error.
+ */
+ protected final void initCloseAction() {
+ final Window window = getDialogPane().getScene().getWindow();
+ window.setOnCloseRequest( event -> window.hide() );
+ }
+}
src/main/java/com/keenwrite/dialogs/ImageDialog.java
+/*
+ * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.dialogs;
+
+import static com.keenwrite.Messages.get;
+import com.keenwrite.controls.BrowseFileButton;
+import com.keenwrite.controls.EscapeTextField;
+import java.nio.file.Path;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.scene.control.ButtonBar.ButtonData;
+import static javafx.scene.control.ButtonType.OK;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Label;
+import javafx.stage.FileChooser.ExtensionFilter;
+import javafx.stage.Window;
+import org.tbee.javafx.scene.layout.fxml.MigPane;
+
+/**
+ * Dialog to enter a markdown image.
+ */
+public class ImageDialog extends AbstractDialog<String> {
+
+ private final StringProperty image = new SimpleStringProperty();
+
+ public ImageDialog( final Window owner, final Path basePath ) {
+ super(owner, "Dialog.image.title" );
+
+ final DialogPane dialogPane = getDialogPane();
+ dialogPane.setContent( pane );
+
+ linkBrowseFileButton.setBasePath( basePath );
+ linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
+ linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
+
+ dialogPane.lookupButton( OK ).disableProperty().bind(
+ urlField.escapedTextProperty().isEmpty()
+ .or( textField.escapedTextProperty().isEmpty() ) );
+
+ image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
+ .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
+ .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
+ previewField.textProperty().bind( image );
+
+ setResultConverter( dialogButton -> {
+ ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
+ return (data == ButtonData.OK_DONE) ? image.get() : null;
+ } );
+
+ Platform.runLater( () -> {
+ urlField.requestFocus();
+
+ if( urlField.getText().startsWith( "http://" ) ) {
+ urlField.selectRange( "http://".length(), urlField.getLength() );
+ }
+ } );
+ }
+
+ @Override
+ protected void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
+ pane = new MigPane();
+ Label urlLabel = new Label();
+ urlField = new EscapeTextField();
+ linkBrowseFileButton = new BrowseFileButton();
+ Label textLabel = new Label();
+ textField = new EscapeTextField();
+ Label titleLabel = new Label();
+ titleField = new EscapeTextField();
+ Label previewLabel = new Label();
+ previewField = new Label();
+
+ //======== pane ========
+ {
+ pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
+ pane.setRows( "[][][][]" );
+
+ //---- urlLabel ----
+ urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
+ pane.add( urlLabel, "cell 0 0" );
+
+ //---- urlField ----
+ urlField.setEscapeCharacters( "()" );
+ urlField.setText( "http://yourlink.com" );
+ urlField.setPromptText( "http://yourlink.com" );
+ pane.add( urlField, "cell 1 0" );
+ pane.add( linkBrowseFileButton, "cell 2 0" );
+
+ //---- textLabel ----
+ textLabel.setText( get( "Dialog.image.textLabel.text" ) );
+ pane.add( textLabel, "cell 0 1" );
+
+ //---- textField ----
+ textField.setEscapeCharacters( "[]" );
+ pane.add( textField, "cell 1 1 2 1" );
+
+ //---- titleLabel ----
+ titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
+ pane.add( titleLabel, "cell 0 2" );
+ pane.add( titleField, "cell 1 2 2 1" );
+
+ //---- previewLabel ----
+ previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
+ pane.add( previewLabel, "cell 0 3" );
+ pane.add( previewField, "cell 1 3 2 1" );
+ }
+ // JFormDesigner - End of component initialization //GEN-END:initComponents
+ }
+
+ // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
+ private MigPane pane;
+ private EscapeTextField urlField;
+ private BrowseFileButton linkBrowseFileButton;
+ private EscapeTextField textField;
+ private EscapeTextField titleField;
+ private Label previewField;
+ // JFormDesigner - End of variables declaration //GEN-END:variables
+}
src/main/java/com/keenwrite/dialogs/ImageDialog.jfd
+JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
+
+new FormModel {
+ "i18n.bundlePackage": "com.scrivendor"
+ "i18n.bundleName": "messages"
+ "i18n.autoExternalize": true
+ "i18n.keyPrefix": "ImageDialog"
+ contentType: "form/javafx"
+ root: new FormRoot {
+ add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
+ "$layoutConstraints": ""
+ "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
+ "$rowConstraints": "[][][][]"
+ } ) {
+ name: "pane"
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "urlLabel"
+ "text": new FormMessage( null, "ImageDialog.urlLabel.text" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
+ name: "urlField"
+ "escapeCharacters": "()"
+ "text": "http://yourlink.com"
+ "promptText": "http://yourlink.com"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 0"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
+ name: "linkBrowseFileButton"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 2 0"
+ } )
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "textLabel"
+ "text": new FormMessage( null, "ImageDialog.textLabel.text" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
+ name: "textField"
+ "escapeCharacters": "[]"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 1 2 1"
+ } )
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "titleLabel"
+ "text": new FormMessage( null, "ImageDialog.titleLabel.text" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 2"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
+ name: "titleField"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 2 2 1"
+ } )
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "previewLabel"
+ "text": new FormMessage( null, "ImageDialog.previewLabel.text" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3"
+ } )
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "previewField"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 3 2 1"
+ } )
+ }, new FormLayoutConstraints( null ) {
+ "location": new javafx.geometry.Point2D( 0.0, 0.0 )
+ "size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
+ } )
+ }
+}
src/main/java/com/keenwrite/dialogs/LinkDialog.java
+/*
+ * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.dialogs;
+
+import com.keenwrite.controls.EscapeTextField;
+import com.keenwrite.editors.markdown.HyperlinkModel;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.Label;
+import javafx.stage.Window;
+import org.tbee.javafx.scene.layout.fxml.MigPane;
+
+import static com.keenwrite.Messages.get;
+import static javafx.scene.control.ButtonType.OK;
+
+/**
+ * Dialog to enter a markdown link.
+ */
+public class LinkDialog extends AbstractDialog<String> {
+
+ private final StringProperty link = new SimpleStringProperty();
+
+ public LinkDialog(
+ final Window owner, final HyperlinkModel hyperlink ) {
+ super( owner, "Dialog.link.title" );
+
+ final DialogPane dialogPane = getDialogPane();
+ dialogPane.setContent( pane );
+
+ dialogPane.lookupButton( OK ).disableProperty().bind(
+ urlField.escapedTextProperty().isEmpty() );
+
+ textField.setText( hyperlink.getText() );
+ urlField.setText( hyperlink.getUrl() );
+ titleField.setText( hyperlink.getTitle() );
+
+ link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
+ .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
+ .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
+ .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
+ .otherwise( urlField.escapedTextProperty() ) ) );
+
+ setResultConverter( dialogButton -> {
+ ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
+ return (data == ButtonData.OK_DONE) ? link.get() : null;
+ } );
+
+ Platform.runLater( () -> {
+ urlField.requestFocus();
+ urlField.selectRange( 0, urlField.getLength() );
+ } );
+ }
+
+ @Override
+ protected void initComponents() {
+ // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
+ pane = new MigPane();
+ Label urlLabel = new Label();
+ urlField = new EscapeTextField();
+ Label textLabel = new Label();
+ textField = new EscapeTextField();
+ Label titleLabel = new Label();
+ titleField = new EscapeTextField();
+
+ //======== pane ========
+ {
+ pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
+ pane.setRows( "[][][][]" );
+
+ //---- urlLabel ----
+ urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
+ pane.add( urlLabel, "cell 0 0" );
+
+ //---- urlField ----
+ urlField.setEscapeCharacters( "()" );
+ pane.add( urlField, "cell 1 0" );
+
+ //---- textLabel ----
+ textLabel.setText( get( "Dialog.link.textLabel.text" ) );
+ pane.add( textLabel, "cell 0 1" );
+
+ //---- textField ----
+ textField.setEscapeCharacters( "[]" );
+ pane.add( textField, "cell 1 1 3 1" );
+
+ //---- titleLabel ----
+ titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
+ pane.add( titleLabel, "cell 0 2" );
+ pane.add( titleField, "cell 1 2 3 1" );
+ }
+ // JFormDesigner - End of component initialization //GEN-END:initComponents
+ }
+
+ // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
+ private MigPane pane;
+ private EscapeTextField urlField;
+ private EscapeTextField textField;
+ private EscapeTextField titleField;
+ // JFormDesigner - End of variables declaration //GEN-END:variables
+}
src/main/java/com/keenwrite/dialogs/LinkDialog.jfd
+JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
+
+new FormModel {
+ "i18n.bundlePackage": "com.scrivendor"
+ "i18n.bundleName": "messages"
+ "i18n.autoExternalize": true
+ "i18n.keyPrefix": "LinkDialog"
+ contentType: "form/javafx"
+ root: new FormRoot {
+ add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
+ "$layoutConstraints": ""
+ "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
+ "$rowConstraints": "[][][][]"
+ } ) {
+ name: "pane"
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "urlLabel"
+ "text": new FormMessage( null, "LinkDialog.urlLabel.text" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 0"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
+ name: "urlField"
+ "escapeCharacters": "()"
+ "text": "http://yourlink.com"
+ "promptText": "http://yourlink.com"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 0"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
+ name: "linkBrowseDirectoyButton"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 2 0"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
+ name: "linkBrowseFileButton"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 3 0"
+ } )
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "textLabel"
+ "text": new FormMessage( null, "LinkDialog.textLabel.text" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 1"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
+ name: "textField"
+ "escapeCharacters": "[]"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 1 3 1"
+ } )
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "titleLabel"
+ "text": new FormMessage( null, "LinkDialog.titleLabel.text" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 2"
+ } )
+ add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
+ name: "titleField"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 2 3 1"
+ } )
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "previewLabel"
+ "text": new FormMessage( null, "LinkDialog.previewLabel.text" )
+ auxiliary() {
+ "JavaCodeGenerator.variableLocal": true
+ }
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 0 3"
+ } )
+ add( new FormComponent( "javafx.scene.control.Label" ) {
+ name: "previewField"
+ }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
+ "value": "cell 1 3 3 1"
+ } )
+ }, new FormLayoutConstraints( null ) {
+ "location": new javafx.geometry.Point2D( 0.0, 0.0 )
+ "size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
+ } )
+ }
+}
src/main/java/com/keenwrite/editors/DefinitionDecoratorFactory.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.editors;
+
+import com.keenwrite.AbstractFileFactory;
+import com.keenwrite.sigils.RSigilOperator;
+import com.keenwrite.sigils.SigilOperator;
+import com.keenwrite.sigils.YamlSigilOperator;
+
+import java.nio.file.Path;
+
+/**
+ * Responsible for creating a definition name decorator suited to a particular
+ * file type.
+ */
+public class DefinitionDecoratorFactory extends AbstractFileFactory {
+
+ private DefinitionDecoratorFactory() {
+ }
+
+ public static SigilOperator newInstance( final Path path ) {
+ final var factory = new DefinitionDecoratorFactory();
+
+ return switch( factory.lookup( path ) ) {
+ case RMARKDOWN, RXML -> new RSigilOperator();
+ default -> new YamlSigilOperator();
+ };
+ }
+}
src/main/java/com/keenwrite/editors/DefinitionNameInjector.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.editors;
+
+import com.keenwrite.FileEditorTab;
+import com.keenwrite.definition.DefinitionPane;
+import com.keenwrite.definition.DefinitionTreeItem;
+import com.keenwrite.sigils.SigilOperator;
+import javafx.scene.control.TreeItem;
+import javafx.scene.input.KeyEvent;
+import org.fxmisc.richtext.StyledTextArea;
+
+import java.nio.file.Path;
+import java.text.BreakIterator;
+
+import static com.keenwrite.Constants.*;
+import static com.keenwrite.StatusBarNotifier.alert;
+import static java.lang.Character.isWhitespace;
+import static javafx.scene.input.KeyCode.SPACE;
+import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
+
+/**
+ * Provides the logic for injecting variable names within the editor.
+ */
+public final class DefinitionNameInjector {
+
+ /**
+ * Recipient of name injections.
+ */
+ private FileEditorTab mTab;
+
+ /**
+ * Initiates double-click events.
+ */
+ private final DefinitionPane mDefinitionPane;
+
+ /**
+ * Initializes the variable name injector against the given pane.
+ *
+ * @param pane The definition panel to listen to for double-click events.
+ */
+ public DefinitionNameInjector( final DefinitionPane pane ) {
+ mDefinitionPane = pane;
+ }
+
+ /**
+ * Trap Control+Space.
+ *
+ * @param tab Editor where variable names get injected.
+ */
+ public void addListener( final FileEditorTab tab ) {
+ assert tab != null;
+ mTab = tab;
+
+ tab.getEditorPane().addKeyboardListener(
+ keyPressed( SPACE, CONTROL_DOWN ),
+ this::autoinsert
+ );
+ }
+
+ /**
+ * Inserts the currently selected variable from the {@link DefinitionPane}.
+ */
+ public void injectSelectedItem() {
+ final var pane = getDefinitionPane();
+ final TreeItem<String> item = pane.getSelectedItem();
+
+ if( item.isLeaf() ) {
+ final var leaf = pane.findLeafExact( item.getValue() );
+ final var editor = getEditor();
+
+ editor.insertText( editor.getCaretPosition(), decorate( leaf ) );
+ }
+ }
+
+ /**
+ * Pressing Control+SPACE will find a node that matches the current word and
+ * substitute the definition reference.
+ */
+ public void autoinsert() {
+ final String paragraph = getCaretParagraph();
+ final int[] bounds = getWordBoundariesAtCaret();
+
+ try {
+ if( isEmptyDefinitionPane() ) {
+ alert( STATUS_DEFINITION_EMPTY );
+ }
+ else {
+ final String word = paragraph.substring( bounds[ 0 ], bounds[ 1 ] );
+
+ if( word.isBlank() ) {
+ alert( STATUS_DEFINITION_BLANK );
+ }
+ else {
+ final var leaf = findLeaf( word );
+
+ if( leaf == null ) {
+ alert( STATUS_DEFINITION_MISSING, word );
+ }
+ else {
+ replaceText( bounds[ 0 ], bounds[ 1 ], decorate( leaf ) );
+ expand( leaf );
+ }
+ }
+ }
+ } catch( final Exception ignored ) {
+ alert( STATUS_DEFINITION_BLANK );
+ }
+ }
+
+ /**
+ * Pressing Control+SPACE will find a node that matches the current word and
+ * substitute the definition reference.
+ *
+ * @param e Ignored -- it can only be Control+SPACE.
+ */
+ private void autoinsert( final KeyEvent e ) {
+ autoinsert();
+ }
+
+ /**
+ * Finds the start and end indexes for the word in the current paragraph
+ * where the caret is located. There are a few different scenarios, where
+ * the caret can be at: the start, end, or middle of a word; also, the
+ * caret can be at the end or beginning of a punctuated word; as well, the
+ * caret could be at the beginning or end of the line or document.
+ */
+ private int[] getWordBoundariesAtCaret() {
+ final var paragraph = getCaretParagraph();
+ final var length = paragraph.length();
+ int offset = getCurrentCaretColumn();
+
+ int began = offset;
+ int ended = offset;
+
+ while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
+ began--;
+ }
+
+ while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
+ ended++;
+ }
+
+ final var iterator = BreakIterator.getWordInstance();
+ iterator.setText( paragraph );
+
+ while( began < length && iterator.isBoundary( began + 1 ) ) {
+ began++;
+ }
+
+ while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
+ ended--;
+ }
+
+ return new int[]{began, ended};
+ }
+
+ /**
+ * Decorates a {@link TreeItem} using the syntax specific to the type of
+ * document being edited.
+ *
+ * @param leaf The path to the leaf (the definition key) to be decorated.
+ */
+ private String decorate( final DefinitionTreeItem<String> leaf ) {
+ return decorate( leaf.toPath() );
+ }
+
+ /**
+ * Decorates a variable using the syntax specific to the type of document
+ * being edited.
+ *
+ * @param variable The variable to decorate in dot-notation without any
+ * start or end sigils present.
+ */
+ private String decorate( final String variable ) {
+ return getVariableDecorator().apply( variable );
+ }
+
+ /**
+ * Updates the text at the given position within the current paragraph.
+ *
+ * @param posBegan The starting index in the paragraph text to replace.
+ * @param posEnded The ending index in the paragraph text to replace.
+ * @param text Overwrite the paragraph substring with this text.
+ */
+ private void replaceText(
+ final int posBegan, final int posEnded, final String text ) {
+ final int p = getCurrentParagraph();
+
+ getEditor().replaceText( p, posBegan, p, posEnded, text );
+ }
+
+ /**
+ * Returns the caret's current paragraph position.
+ *
+ * @return A number greater than or equal to 0.
+ */
+ private int getCurrentParagraph() {
+ return getEditor().getCurrentParagraph();
+ }
+
+ /**
+ * Returns the text for the paragraph that contains the caret.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ private String getCaretParagraph() {
+ return getEditor().getText( getCurrentParagraph() );
+ }
+
+ /**
+ * Returns the caret position within the current paragraph.
+ *
+ * @return A value from 0 to the length of the current paragraph.
+ */
+ private int getCurrentCaretColumn() {
+ return getEditor().getCaretColumn();
+ }
+
+ /**
+ * Looks for the given word, matching first by exact, next by a starts-with
+ * condition with diacritics replaced, then by containment.
+ *
+ * @param word The word to match by: exact, at the beginning, or containment.
+ * @return The matching {@link DefinitionTreeItem} for the given word, or
+ * {@code null} if none found.
+ */
+ @SuppressWarnings("ConstantConditions")
+ private DefinitionTreeItem<String> findLeaf( final String word ) {
+ assert word != null;
+
+ final var pane = getDefinitionPane();
+ DefinitionTreeItem<String> leaf = null;
+
+ leaf = leaf == null ? pane.findLeafExact( word ) : leaf;
+ leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf;
+ leaf = leaf == null ? pane.findLeafContains( word ) : leaf;
+ leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf;
+
+ return leaf;
+ }
+
+ /**
+ * Answers whether there are any definitions in the tree.
+ *
+ * @return {@code true} when there are no definitions; {@code false} when
+ * there's at least one definition.
+ */
+ private boolean isEmptyDefinitionPane() {
+ return getDefinitionPane().isEmpty();
+ }
+
+ /**
+ * Collapses the tree then expands and selects the given node.
+ *
+ * @param node The node to expand.
+ */
+ private void expand( final TreeItem<String> node ) {
+ final DefinitionPane pane = getDefinitionPane();
+ pane.collapse();
+ pane.expand( node );
+ pane.select( node );
+ }
+
+ /**
+ * @return A variable decorator that corresponds to the given file type.
+ */
+ private SigilOperator getVariableDecorator() {
+ return DefinitionDecoratorFactory.newInstance( getFilename() );
+ }
+
+ private Path getFilename() {
+ return getFileEditorTab().getPath();
+ }
+
+ private EditorPane getEditorPane() {
+ return getFileEditorTab().getEditorPane();
+ }
+
+ private StyledTextArea<?, ?> getEditor() {
+ return getEditorPane().getEditor();
+ }
+
+ public FileEditorTab getFileEditorTab() {
+ return mTab;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ return mDefinitionPane;
+ }
+}
src/main/java/com/keenwrite/editors/EditorPane.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.editors;
+
+import com.keenwrite.preferences.UserPreferences;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.event.Event;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.layout.Pane;
+import org.fxmisc.flowless.VirtualizedScrollPane;
+import org.fxmisc.richtext.StyleClassedTextArea;
+import org.fxmisc.undo.UndoManager;
+import org.fxmisc.wellbehaved.event.EventPattern;
+import org.fxmisc.wellbehaved.event.Nodes;
+
+import java.nio.file.Path;
+import java.util.function.Consumer;
+
+import static com.keenwrite.StatusBarNotifier.clearAlert;
+import static java.lang.String.format;
+import static javafx.application.Platform.runLater;
+import static org.fxmisc.wellbehaved.event.InputMap.consume;
+
+/**
+ * Represents common editing features for various types of text editors.
+ */
+public class EditorPane extends Pane {
+
+ /**
+ * Used when changing the text area font size.
+ */
+ private static final String FMT_CSS_FONT_SIZE = "-fx-font-size: %dpt;";
+
+ private final StyleClassedTextArea mEditor =
+ new StyleClassedTextArea( false );
+ private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
+ new VirtualizedScrollPane<>( mEditor );
+ private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>();
+
+ public EditorPane() {
+ getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
+ fontsSizeProperty().addListener(
+ ( l, o, n ) -> setFontSize( n.intValue() )
+ );
+
+ // Clear out any previous alerts after the user has typed. If the problem
+ // persists, re-rendering the document will re-raise the error. If there
+ // was no previous error, clearing the alert is essentially a no-op.
+ mEditor.textProperty().addListener(
+ ( l, o, n ) -> clearAlert()
+ );
+ }
+
+ @Override
+ public void requestFocus() {
+ requestFocus( 3 );
+ }
+
+ /**
+ * There's a race-condition between displaying the {@link EditorPane}
+ * and giving the {@link #mEditor} focus. Try to focus up to {@code max}
+ * times before giving up.
+ *
+ * @param max The number of attempts to try to request focus.
+ */
+ private void requestFocus( final int max ) {
+ if( max > 0 ) {
+ runLater(
+ () -> {
+ final var editor = getEditor();
+
+ if( !editor.isFocused() ) {
+ editor.requestFocus();
+ requestFocus( max - 1 );
+ }
+ }
+ );
+ }
+ }
+
+ public void undo() {
+ getUndoManager().undo();
+ }
+
+ public void redo() {
+ getUndoManager().redo();
+ }
+
+ /**
+ * Cuts the actively selected text; if no text is selected, this will cut
+ * the entire paragraph.
+ */
+ public void cut() {
+ final var editor = getEditor();
+ final var selected = editor.getSelectedText();
+
+ if( selected == null || selected.isEmpty() ) {
+ editor.selectParagraph();
+ }
+
+ editor.cut();
+ }
+
+ public void copy() {
+ getEditor().copy();
+ }
+
+ public void paste() {
+ getEditor().paste();
+ }
+
+ public void selectAll() {
+ getEditor().selectAll();
+ }
+
+ public UndoManager<?> getUndoManager() {
+ return getEditor().getUndoManager();
+ }
+
+ public String getText() {
+ return getEditor().getText();
+ }
+
+ public void setText( final String text ) {
+ final var editor = getEditor();
+ editor.deselect();
+ editor.replaceText( text );
+ getUndoManager().mark();
+ }
+
+ /**
+ * Call to hook into changes to the text area.
+ *
+ * @param listener Receives editor text change events.
+ */
+ public void addTextChangeListener(
+ final ChangeListener<? super String> listener ) {
+ getEditor().textProperty().addListener( listener );
+ }
+
+ /**
+ * Notifies observers when the caret changes paragraph.
+ *
+ * @param listener Receives change event.
+ */
+ public void addCaretParagraphListener(
+ final ChangeListener<? super Integer> listener ) {
+ getEditor().currentParagraphProperty().addListener( listener );
+ }
+
+ /**
+ * Notifies observers when the caret changes position.
+ *
+ * @param listener Receives change event.
+ */
+ public void addCaretPositionListener(
+ final ChangeListener<? super Integer> listener ) {
+ getEditor().caretPositionProperty().addListener( listener );
+ }
+
+ /**
+ * This method adds listeners to editor events.
+ *
+ * @param <T> The event type.
+ * @param <U> The consumer type for the given event type.
+ * @param event The event of interest.
+ * @param consumer The method to call when the event happens.
+ */
+ public <T extends Event, U extends T> void addKeyboardListener(
+ final EventPattern<? super T, ? extends U> event,
+ final Consumer<? super U> consumer ) {
+ Nodes.addInputMap( getEditor(), consume( event, consumer ) );
+ }
+
+ /**
+ * Repositions the cursor and scroll bar to the top of the file.
+ */
+ public void scrollToTop() {
+ getEditor().moveTo( 0 );
+ getScrollPane().scrollYToPixel( 0 );
+ }
+
+ public StyleClassedTextArea getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * Returns the scroll pane that contains the text area.
+ *
+ * @return The scroll pane that contains the content to edit.
+ */
+ public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
+ return mScrollPane;
+ }
+
+ public Path getPath() {
+ return mPath.get();
+ }
+
+ public void setPath( final Path path ) {
+ mPath.set( path );
+ }
+
+ /**
+ * Sets the font size in points.
+ *
+ * @param size The new font size to use for the text editor.
+ */
+ private void setFontSize( final int size ) {
+ mEditor.setStyle( format( FMT_CSS_FONT_SIZE, size ) );
+ }
+
+ /**
+ * Returns the text editor font size property for handling font size change
+ * events.
+ */
+ private IntegerProperty fontsSizeProperty() {
+ return UserPreferences.getInstance().fontsSizeEditorProperty();
+ }
+}
src/main/java/com/keenwrite/editors/markdown/HyperlinkModel.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.editors.markdown;
+
+import com.vladsch.flexmark.ast.Link;
+
+/**
+ * Represents the model for a hyperlink: text, url, and title.
+ */
+public class HyperlinkModel {
+
+ private String text;
+ private String url;
+ private String title;
+
+ /**
+ * Constructs a new hyperlink model in Markdown format by default with no
+ * title (i.e., tooltip).
+ *
+ * @param text The hyperlink text displayed (e.g., displayed to the user).
+ * @param url The destination URL (e.g., when clicked).
+ */
+ public HyperlinkModel( final String text, final String url ) {
+ this( text, url, null );
+ }
+
+ /**
+ * Constructs a new hyperlink model for the given AST link.
+ *
+ * @param link A markdown link.
+ */
+ public HyperlinkModel( final Link link ) {
+ this(
+ link.getText().toString(),
+ link.getUrl().toString(),
+ link.getTitle().toString()
+ );
+ }
+
+ /**
+ * Constructs a new hyperlink model in Markdown format by default.
+ *
+ * @param text The hyperlink text displayed (e.g., displayed to the user).
+ * @param url The destination URL (e.g., when clicked).
+ * @param title The hyperlink title (e.g., shown as a tooltip).
+ */
+ public HyperlinkModel( final String text, final String url,
+ final String title ) {
+ setText( text );
+ setUrl( url );
+ setTitle( title );
+ }
+
+ /**
+ * Returns the string in Markdown format by default.
+ *
+ * @return A markdown version of the hyperlink.
+ */
+ @Override
+ public String toString() {
+ String format = "%s%s%s";
+
+ if( hasText() ) {
+ format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
+ }
+
+ // Becomes ""+URL+"" if no text is set.
+ // Becomes [TITLE]+(URL)+"" if no title is set.
+ // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
+ return String.format( format, getText(), getUrl(), getTitle() );
+ }
+
+ public final void setText( final String text ) {
+ this.text = nullSafe( text );
+ }
+
+ public final void setUrl( final String url ) {
+ this.url = nullSafe( url );
+ }
+
+ public final void setTitle( final String title ) {
+ this.title = nullSafe( title );
+ }
+
+ /**
+ * Answers whether text has been set for the hyperlink.
+ *
+ * @return true This is a text link.
+ */
+ public boolean hasText() {
+ return !getText().isEmpty();
+ }
+
+ /**
+ * Answers whether a title (tooltip) has been set for the hyperlink.
+ *
+ * @return true There is a title.
+ */
+ public boolean hasTitle() {
+ return !getTitle().isEmpty();
+ }
+
+ public String getText() {
+ return this.text;
+ }
+
+ public String getUrl() {
+ return this.url;
+ }
+
+ public String getTitle() {
+ return this.title;
+ }
+
+ private String nullSafe( final String s ) {
+ return s == null ? "" : s;
+ }
+}
src/main/java/com/keenwrite/editors/markdown/LinkVisitor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.editors.markdown;
+
+import com.vladsch.flexmark.ast.Link;
+import com.vladsch.flexmark.util.ast.Node;
+import com.vladsch.flexmark.util.ast.NodeVisitor;
+import com.vladsch.flexmark.util.ast.VisitHandler;
+
+/**
+ * Responsible for extracting a hyperlink from the document so that the user
+ * can edit the link within a dialog.
+ */
+public class LinkVisitor {
+
+ private NodeVisitor visitor;
+ private Link link;
+ private final int offset;
+
+ /**
+ * Creates a hyperlink given an offset into a paragraph and the markdown AST
+ * link node.
+ *
+ * @param index Index into the paragraph that indicates the hyperlink to
+ * change.
+ */
+ public LinkVisitor( final int index ) {
+ this.offset = index;
+ }
+
+ public Link process( final Node root ) {
+ getVisitor().visit( root );
+ return getLink();
+ }
+
+ /**
+ * @param link Not null.
+ */
+ private void visit( final Link link ) {
+ final int began = link.getStartOffset();
+ final int ended = link.getEndOffset();
+ final int index = getOffset();
+
+ if( index >= began && index <= ended ) {
+ setLink( link );
+ }
+ }
+
+ private synchronized NodeVisitor getVisitor() {
+ if( this.visitor == null ) {
+ this.visitor = createVisitor();
+ }
+
+ return this.visitor;
+ }
+
+ protected NodeVisitor createVisitor() {
+ return new NodeVisitor(
+ new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
+ }
+
+ private Link getLink() {
+ return this.link;
+ }
+
+ private void setLink( final Link link ) {
+ this.link = link;
+ }
+
+ public int getOffset() {
+ return this.offset;
+ }
+}
src/main/java/com/keenwrite/editors/markdown/MarkdownEditorPane.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.editors.markdown;
+
+import com.keenwrite.dialogs.ImageDialog;
+import com.keenwrite.dialogs.LinkDialog;
+import com.keenwrite.editors.EditorPane;
+import com.keenwrite.processors.markdown.BlockExtension;
+import com.keenwrite.processors.markdown.MarkdownProcessor;
+import com.vladsch.flexmark.ast.Link;
+import com.vladsch.flexmark.html.renderer.AttributablePart;
+import com.vladsch.flexmark.util.ast.Node;
+import com.vladsch.flexmark.util.html.MutableAttributes;
+import javafx.scene.control.Dialog;
+import javafx.scene.control.IndexRange;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.stage.Window;
+import org.fxmisc.richtext.StyleClassedTextArea;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.keenwrite.Constants.STYLESHEET_MARKDOWN;
+import static com.keenwrite.util.Utils.ltrim;
+import static com.keenwrite.util.Utils.rtrim;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
+
+/**
+ * Provides the ability to edit a text document.
+ */
+public class MarkdownEditorPane extends EditorPane {
+ private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
+ "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
+
+ /**
+ * Any of these followed by a space and a letter produce a line
+ * by themselves. The ">" need not be followed by a space.
+ */
+ private static final Pattern PATTERN_NEW_LINE = Pattern.compile(
+ "^>|(((#+)|([*+\\-])|([1-9]\\.))\\s+).+" );
+
+ public MarkdownEditorPane() {
+ initEditor();
+ }
+
+ private void initEditor() {
+ final StyleClassedTextArea textArea = getEditor();
+
+ textArea.setWrapText( true );
+ textArea.getStyleClass().add( "markdown-editor" );
+ textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
+
+ addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
+ addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut );
+ }
+
+ public void insertLink() {
+ insertObject( createLinkDialog() );
+ }
+
+ public void insertImage() {
+ insertObject( createImageDialog() );
+ }
+
+ /**
+ * Returns the editor's paragraph number that will be close to its HTML
+ * paragraph ID. Ultimately this solution is flawed because there isn't
+ * a straightforward correlation between the document being edited and
+ * what is rendered. XML documents transformed through stylesheets have
+ * no readily determined correlation. Images, tables, and other
+ * objects affect the relative location of the current paragraph being
+ * edited with respect to the preview pane.
+ * <p>
+ * See
+ * {@link BlockExtension.IdAttributeProvider#setAttributes(Node, AttributablePart, MutableAttributes)}}
+ * for details.
+ * </p>
+ * <p>
+ * Injecting a token into the document, as per a previous version of the
+ * application, can instruct the preview pane where to shift the viewport.
+ * </p>
+ *
+ * @param paraIndex The paragraph index from the editor pane to scroll to
+ * in the preview pane, which will be approximated if an
+ * equivalent cannot be found.
+ * @return A unique identifier that correlates to an equivalent paragraph
+ * number once the Markdown is rendered into HTML.
+ */
+ public int approximateParagraphId( final int paraIndex ) {
+ final StyleClassedTextArea editor = getEditor();
+ final List<String> lines = new ArrayList<>( 4096 );
+
+ int i = 0;
+ String prevText = "";
+ boolean withinFencedBlock = false;
+ boolean withinCodeBlock = false;
+
+ for( final var p : editor.getParagraphs() ) {
+ if( i > paraIndex ) {
+ break;
+ }
+
+ final String text = p.getText().replace( '>', ' ' );
+ if( text.startsWith( "```" ) ) {
+ if( withinFencedBlock = !withinFencedBlock ) {
+ lines.add( text );
+ }
+ }
+
+ if( !withinFencedBlock ) {
+ final boolean foundCodeBlock = text.startsWith( " " );
+
+ if( foundCodeBlock && !withinCodeBlock ) {
+ lines.add( text );
+ withinCodeBlock = true;
+ }
+ else if( !foundCodeBlock ) {
+ withinCodeBlock = false;
+ }
+ }
+
+ if( !withinFencedBlock && !withinCodeBlock &&
+ ((!text.isBlank() && prevText.isBlank()) ||
+ PATTERN_NEW_LINE.matcher( text ).matches()) ) {
+ lines.add( text );
+ }
+
+ prevText = text;
+ i++;
+ }
+
+ // Scrolling index is 1-based.
+ return Math.max( lines.size() - 1, 0 );
+ }
+
+ /**
+ * Gets the index of the paragraph where the caret is positioned.
+ *
+ * @return The paragraph number for the caret.
+ */
+ public int getCurrentParagraphIndex() {
+ return getEditor().getCurrentParagraph();
+ }
+
+ /**
+ * @param leading Characters to insert at the beginning of the current
+ * selection (or paragraph).
+ * @param trailing Characters to insert at the end of the current selection
+ * (or paragraph).
+ */
+ public void surroundSelection( final String leading, final String trailing ) {
+ surroundSelection( leading, trailing, null );
+ }
+
+ /**
+ * @param leading Characters to insert at the beginning of the current
+ * selection (or paragraph).
+ * @param trailing Characters to insert at the end of the current selection
+ * (or paragraph).
+ * @param hint Instructional text inserted within the leading and
+ * trailing characters, provided no text is selected.
+ */
+ public void surroundSelection(
+ String leading, String trailing, final String hint ) {
+ final StyleClassedTextArea textArea = getEditor();
+
+ // Note: not using textArea.insertText() to insert leading and trailing
+ // because this would add two changes to undo history
+ final IndexRange selection = textArea.getSelection();
+ int start = selection.getStart();
+ int end = selection.getEnd();
+
+ final String selectedText = textArea.getSelectedText();
+
+ String trimmedText = selectedText.trim();
+ if( trimmedText.length() < selectedText.length() ) {
+ start += selectedText.indexOf( trimmedText );
+ end = start + trimmedText.length();
+ }
+
+ // remove leading whitespaces from leading text if selection starts at zero
+ if( start == 0 ) {
+ leading = ltrim( leading );
+ }
+
+ // remove trailing whitespaces from trailing text if selection ends at
+ // text end
+ if( end == textArea.getLength() ) {
+ trailing = rtrim( trailing );
+ }
+
+ // remove leading line separators from leading text
+ // if there are line separators before the selected text
+ if( leading.startsWith( "\n" ) ) {
+ for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
+ if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
+ break;
+ }
+
+ leading = leading.substring( 1 );
+ }
+ }
+
+ // remove trailing line separators from trailing or leading text
+ // if there are line separators after the selected text
+ final boolean trailingIsEmpty = trailing.isEmpty();
+ String str = trailingIsEmpty ? leading : trailing;
+
+ if( str.endsWith( "\n" ) ) {
+ final int length = textArea.getLength();
+
+ for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
+ if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
+ break;
+ }
+
+ str = str.substring( 0, str.length() - 1 );
+ }
+
+ if( trailingIsEmpty ) {
+ leading = str;
+ }
+ else {
+ trailing = str;
+ }
+ }
+
+ int selStart = start + leading.length();
+ int selEnd = end + leading.length();
+
+ // insert hint text if selection is empty
+ if( hint != null && trimmedText.isEmpty() ) {
+ trimmedText = hint;
+ selEnd = selStart + hint.length();
+ }
+
+ // prevent undo merging with previous text entered by user
+ getUndoManager().preventMerge();
+
+ // replace text and update selection
+ textArea.replaceText( start, end, leading + trimmedText + trailing );
+ textArea.selectRange( selStart, selEnd );
+ }
+
+ private void enterPressed( final KeyEvent e ) {
+ final StyleClassedTextArea textArea = getEditor();
+ final String currentLine =
+ textArea.getText( textArea.getCurrentParagraph() );
+ final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
+
+ String newText = "\n";
+
+ if( matcher.matches() ) {
+ if( !matcher.group( 2 ).isEmpty() ) {
+ // indent new line with same whitespace characters and list markers
+ // as current line
+ newText = newText.concat( matcher.group( 1 ) );
+ }
+ else {
+ // current line contains only whitespace characters and list markers
+ // --> empty current line
+ final int caretPosition = textArea.getCaretPosition();
+ textArea.selectRange( caretPosition - currentLine.length(),
+ caretPosition );
+ }
+ }
+
+ textArea.replaceSelection( newText );
+
+ // Ensure that the window scrolls when Enter is pressed at the bottom of
+ // the pane.
+ textArea.requestFollowCaret();
+ }
+
+ private void cut( final KeyEvent event ) {
+ super.cut();
+ }
+
+ /**
+ * Returns one of: selected text, word under cursor, or parsed hyperlink from
+ * the markdown AST.
+ *
+ * @return An instance containing the link URL and display text.
+ */
+ private HyperlinkModel getHyperlink() {
+ final StyleClassedTextArea textArea = getEditor();
+ final String selectedText = textArea.getSelectedText();
+
+ // Get the current paragraph, convert to Markdown nodes.
+ final MarkdownProcessor mp = new MarkdownProcessor( null );
+ final int p = textArea.getCurrentParagraph();
+ final String paragraph = textArea.getText( p );
+ final Node node = mp.toNode( paragraph );
+ final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
+ final Link link = visitor.process( node );
+
+ if( link != null ) {
+ textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
+ }
+
+ return createHyperlinkModel(
+ link, selectedText, "https://localhost"
+ );
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private HyperlinkModel createHyperlinkModel(
+ final Link link, final String selection, final String url ) {
+
+ return link == null
+ ? new HyperlinkModel( selection, url )
+ : new HyperlinkModel( link );
+ }
+
+ private Path getParentPath() {
+ final Path path = getPath();
+ return (path != null) ? path.getParent() : null;
+ }
+
+ private Dialog<String> createLinkDialog() {
+ return new LinkDialog( getWindow(), getHyperlink() );
+ }
+
+ private Dialog<String> createImageDialog() {
+ return new ImageDialog( getWindow(), getParentPath() );
+ }
+
+ private void insertObject( final Dialog<String> dialog ) {
+ dialog.showAndWait().ifPresent(
+ result -> getEditor().replaceSelection( result )
+ );
+ }
+
+ private Window getWindow() {
+ return getScrollPane().getScene().getWindow();
+ }
+}
src/main/java/com/keenwrite/predicates/PredicateFactory.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.predicates;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.function.Predicate;
+
+import static java.lang.String.join;
+import static java.nio.file.FileSystems.getDefault;
+
+/**
+ * Provides a number of simple {@link Predicate} instances for various types
+ * of string comparisons, including basic strings and file name strings.
+ */
+public class PredicateFactory {
+ /**
+ * Creates an instance of {@link Predicate} that matches a globbed file
+ * name pattern.
+ *
+ * @param pattern The file name pattern to match.
+ * @return A {@link Predicate} that can answer whether a given file name
+ * matches the given glob pattern.
+ */
+ public static Predicate<File> createFileTypePredicate(
+ final String pattern ) {
+ final var matcher = getDefault().getPathMatcher(
+ "glob:**{" + pattern + "}"
+ );
+
+ return file -> matcher.matches( file.toPath() );
+ }
+
+ /**
+ * Creates an instance of {@link Predicate} that matches any file name from
+ * a {@link Collection} of file name patterns. The given patterns are joined
+ * with commas into a single comma-separated list.
+ *
+ * @param patterns The file name patterns to be matched.
+ * @return A {@link Predicate} that can answer whether a given file name
+ * matches the given glob patterns.
+ */
+ public static Predicate<File> createFileTypePredicate(
+ final Collection<String> patterns ) {
+ return createFileTypePredicate( join( ",", patterns ) );
+ }
+
+ /**
+ * Creates an instance of {@link Predicate} that compares whether the given
+ * {@code reference} string is contained by the comparator. Comparison is
+ * case-insensitive. The test will also pass if the comparate is empty.
+ *
+ * @param comparator The string to check as being contained.
+ * @return A {@link Predicate} that can answer whether the given string
+ * is contained within the comparator, or the comparate is empty.
+ */
+ public static Predicate<String> createStringContainsPredicate(
+ final String comparator ) {
+ return comparate -> comparate.isEmpty() ||
+ comparate.toLowerCase().contains( comparator.toLowerCase() );
+ }
+ /**
+ * Creates an instance of {@link Predicate} that compares whether the given
+ * {@code reference} string is starts with the comparator. Comparison is
+ * case-insensitive.
+ *
+ * @param comparator The string to check as being contained.
+ * @return A {@link Predicate} that can answer whether the given string
+ * is contained within the comparator.
+ */
+ public static Predicate<String> createStringStartsPredicate(
+ final String comparator ) {
+ return comparate ->
+ comparate.toLowerCase().startsWith( comparator.toLowerCase() );
+ }
+}
src/main/java/com/keenwrite/preferences/FilePreferences.java
+/*
+ * Copyright 2016 David Croft and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preferences;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.*;
+import java.util.prefs.AbstractPreferences;
+import java.util.prefs.BackingStoreException;
+
+import static com.keenwrite.StatusBarNotifier.alert;
+
+/**
+ * Preferences implementation that stores to a user-defined file. Local file
+ * storage is preferred over a certain operating system's monolithic trash heap
+ * called a registry. When the OS is locked down, the default Preferences
+ * implementation will try to write to the registry and fail due to permissions
+ * problems. This class sidesteps the issue entirely by writing to the user's
+ * home directory, where permissions should be a bit more lax.
+ */
+public class FilePreferences extends AbstractPreferences {
+
+ private final Map<String, String> mRoot = new TreeMap<>();
+ private final Map<String, FilePreferences> mChildren = new TreeMap<>();
+ private boolean mRemoved;
+
+ private final Object mMutex = new Object();
+
+ public FilePreferences(
+ final AbstractPreferences parent, final String name ) {
+ super( parent, name );
+
+ try {
+ sync();
+ } catch( final BackingStoreException ex ) {
+ alert( ex );
+ }
+ }
+
+ @Override
+ protected void putSpi( final String key, final String value ) {
+ synchronized( mMutex ) {
+ mRoot.put( key, value );
+ }
+
+ try {
+ flush();
+ } catch( final BackingStoreException ex ) {
+ alert( ex );
+ }
+ }
+
+ @Override
+ protected String getSpi( final String key ) {
+ synchronized( mMutex ) {
+ return mRoot.get( key );
+ }
+ }
+
+ @Override
+ protected void removeSpi( final String key ) {
+ synchronized( mMutex ) {
+ mRoot.remove( key );
+ }
+
+ try {
+ flush();
+ } catch( final BackingStoreException ex ) {
+ alert( ex );
+ }
+ }
+
+ @Override
+ protected void removeNodeSpi() throws BackingStoreException {
+ mRemoved = true;
+ flush();
+ }
+
+ @Override
+ protected String[] keysSpi() {
+ synchronized( mMutex ) {
+ return mRoot.keySet().toArray( new String[ 0 ] );
+ }
+ }
+
+ @Override
+ protected String[] childrenNamesSpi() {
+ return mChildren.keySet().toArray( new String[ 0 ] );
+ }
+
+ @Override
+ protected FilePreferences childSpi( final String name ) {
+ FilePreferences child = mChildren.get( name );
+
+ if( child == null || child.isRemoved() ) {
+ child = new FilePreferences( this, name );
+ mChildren.put( name, child );
+ }
+
+ return child;
+ }
+
+ @Override
+ protected void syncSpi() {
+ if( isRemoved() ) {
+ return;
+ }
+
+ final File file = FilePreferencesFactory.getPreferencesFile();
+
+ if( !file.exists() ) {
+ return;
+ }
+
+ synchronized( mMutex ) {
+ final Properties p = new Properties();
+
+ try( final var inputStream = new FileInputStream( file ) ) {
+ p.load( inputStream );
+
+ final String path = getPath();
+ final Enumeration<?> propertyNames = p.propertyNames();
+
+ while( propertyNames.hasMoreElements() ) {
+ final String propKey = (String) propertyNames.nextElement();
+
+ if( propKey.startsWith( path ) ) {
+ final String subKey = propKey.substring( path.length() );
+
+ // Only load immediate descendants
+ if( subKey.indexOf( '.' ) == -1 ) {
+ mRoot.put( subKey, p.getProperty( propKey ) );
+ }
+ }
+ }
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+ }
+
+ private String getPath() {
+ final FilePreferences parent = (FilePreferences) parent();
+
+ return parent == null ? "" : parent.getPath() + name() + '.';
+ }
+
+ @Override
+ protected void flushSpi() {
+ final File file = FilePreferencesFactory.getPreferencesFile();
+
+ synchronized( mMutex ) {
+ final Properties p = new Properties();
+
+ try {
+ final String path = getPath();
+
+ if( file.exists() ) {
+ try( final var fis = new FileInputStream( file ) ) {
+ p.load( fis );
+ }
+
+ final List<String> toRemove = new ArrayList<>();
+
+ // Make a list of all direct children of this node to be removed
+ final Enumeration<?> propertyNames = p.propertyNames();
+
+ while( propertyNames.hasMoreElements() ) {
+ final String propKey = (String) propertyNames.nextElement();
+ if( propKey.startsWith( path ) ) {
+ final String subKey = propKey.substring( path.length() );
+
+ // Only do immediate descendants
+ if( subKey.indexOf( '.' ) == -1 ) {
+ toRemove.add( propKey );
+ }
+ }
+ }
+
+ // Remove them now that the enumeration is done with
+ for( final String propKey : toRemove ) {
+ p.remove( propKey );
+ }
+ }
+
+ // If this node hasn't been removed, add back in any values
+ if( !mRemoved ) {
+ for( final String s : mRoot.keySet() ) {
+ p.setProperty( path + s, mRoot.get( s ) );
+ }
+ }
+
+ try( final var fos = new FileOutputStream( file ) ) {
+ p.store( fos, "FilePreferences" );
+ }
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+ }
+}
src/main/java/com/keenwrite/preferences/FilePreferencesFactory.java
+/*
+ * Copyright 2016 David Croft and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preferences;
+
+import java.io.File;
+import java.nio.file.FileSystems;
+import java.util.prefs.Preferences;
+import java.util.prefs.PreferencesFactory;
+
+import static com.keenwrite.Constants.APP_TITLE;
+
+/**
+ * PreferencesFactory implementation that stores the preferences in a
+ * user-defined file. Usage:
+ * <pre>
+ * System.setProperty( "java.util.prefs.PreferencesFactory",
+ * FilePreferencesFactory.class.getName() );
+ * </pre>
+ */
+public class FilePreferencesFactory implements PreferencesFactory {
+
+ private static File preferencesFile;
+ private Preferences rootPreferences;
+
+ @Override
+ public Preferences systemRoot() {
+ return userRoot();
+ }
+
+ @Override
+ public synchronized Preferences userRoot() {
+ if( rootPreferences == null ) {
+ rootPreferences = new FilePreferences( null, "" );
+ }
+
+ return rootPreferences;
+ }
+
+ public synchronized static File getPreferencesFile() {
+ if( preferencesFile == null ) {
+ String prefsFile = getPreferencesFilename();
+
+ preferencesFile = new File( prefsFile ).getAbsoluteFile();
+ }
+
+ return preferencesFile;
+ }
+
+ public static String getPreferencesFilename() {
+ final String filename = System.getProperty( "application.name", APP_TITLE );
+ return System.getProperty( "user.home" ) + getSeparator() + "." + filename;
+ }
+
+ public static String getSeparator() {
+ return FileSystems.getDefault().getSeparator();
+ }
+}
src/main/java/com/keenwrite/preferences/UserPreferences.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preferences;
+
+import com.dlsc.formsfx.model.structure.StringField;
+import com.dlsc.preferencesfx.PreferencesFx;
+import com.dlsc.preferencesfx.PreferencesFxEvent;
+import com.dlsc.preferencesfx.model.Category;
+import com.dlsc.preferencesfx.model.Group;
+import com.dlsc.preferencesfx.model.Setting;
+import javafx.beans.property.*;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+
+import java.io.File;
+import java.nio.file.Path;
+
+import static com.keenwrite.Constants.*;
+import static com.keenwrite.Messages.get;
+
+/**
+ * Responsible for user preferences that can be changed from the GUI. The
+ * settings are displayed and persisted using {@link PreferencesFx}.
+ */
+public class UserPreferences {
+ /**
+ * Implementation of the initialization-on-demand holder design pattern,
+ * an for a lazy-loaded singleton. In all versions of Java, the idiom enables
+ * a safe, highly concurrent lazy initialization of static fields with good
+ * performance. The implementation relies upon the initialization phase of
+ * execution within the Java Virtual Machine (JVM) as specified by the Java
+ * Language Specification. When the class {@link UserPreferencesContainer}
+ * is loaded, its initialization completes trivially because there are no
+ * static variables to initialize.
+ * <p>
+ * The static class definition {@link UserPreferencesContainer} within the
+ * {@link UserPreferences} is not initialized until such time that
+ * {@link UserPreferencesContainer} must be executed. The static
+ * {@link UserPreferencesContainer} class executes when
+ * {@link #getInstance} is called. The first call will trigger loading and
+ * initialization of the {@link UserPreferencesContainer} thereby
+ * instantiating the {@link #INSTANCE}.
+ * </p>
+ * <p>
+ * This indirection is necessary because the {@link UserPreferences} class
+ * references {@link PreferencesFx}, which must not be instantiated until the
+ * UI is ready.
+ * </p>
+ */
+ private static class UserPreferencesContainer {
+ private static final UserPreferences INSTANCE = new UserPreferences();
+ }
+
+ public static UserPreferences getInstance() {
+ return UserPreferencesContainer.INSTANCE;
+ }
+
+ private final PreferencesFx mPreferencesFx;
+
+ private final ObjectProperty<File> mPropRDirectory;
+ private final StringProperty mPropRScript;
+ private final ObjectProperty<File> mPropImagesDirectory;
+ private final StringProperty mPropImagesOrder;
+ private final ObjectProperty<File> mPropDefinitionPath;
+ private final StringProperty mRDelimiterBegan;
+ private final StringProperty mRDelimiterEnded;
+ private final StringProperty mDefDelimiterBegan;
+ private final StringProperty mDefDelimiterEnded;
+ private final IntegerProperty mPropFontsSizeEditor;
+
+ private UserPreferences() {
+ mPropRDirectory = simpleFile( USER_DIRECTORY );
+ mPropRScript = new SimpleStringProperty( "" );
+
+ mPropImagesDirectory = simpleFile( USER_DIRECTORY );
+ mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT );
+
+ mPropDefinitionPath = simpleFile(
+ getSetting( "file.definition.default", DEFINITION_NAME )
+ );
+
+ mDefDelimiterBegan = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT );
+ mDefDelimiterEnded = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT );
+
+ mRDelimiterBegan = new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT );
+ mRDelimiterEnded = new SimpleStringProperty( R_DELIM_ENDED_DEFAULT );
+
+ mPropFontsSizeEditor = new SimpleIntegerProperty( (int) FONT_SIZE_EDITOR );
+
+ // All properties must be initialized before creating the dialog.
+ mPreferencesFx = createPreferencesFx();
+ }
+
+ /**
+ * Display the user preferences settings dialog (non-modal).
+ */
+ public void show() {
+ getPreferencesFx().show( false );
+ }
+
+ /**
+ * Call to persist the settings. Strictly speaking, this could watch on
+ * all values for external changes then save automatically.
+ */
+ public void save() {
+ getPreferencesFx().saveSettings();
+ }
+
+ /**
+ * Creates the preferences dialog.
+ * <p>
+ * TODO: Make this dynamic by iterating over all "Preferences.*" values
+ * that follow a particular naming pattern.
+ * </p>
+ *
+ * @return A new instance of preferences for users to edit.
+ */
+ @SuppressWarnings("unchecked")
+ private PreferencesFx createPreferencesFx() {
+ final Setting<StringField, StringProperty> scriptSetting =
+ Setting.of( "Script", mPropRScript );
+ final StringField field = scriptSetting.getElement();
+ field.multiline( true );
+
+ return PreferencesFx.of(
+ UserPreferences.class,
+ Category.of(
+ get( "Preferences.r" ),
+ Group.of(
+ get( "Preferences.r.directory" ),
+ Setting.of( label( "Preferences.r.directory.desc", false ) ),
+ Setting.of( "Directory", mPropRDirectory, true )
+ ),
+ Group.of(
+ get( "Preferences.r.script" ),
+ Setting.of( label( "Preferences.r.script.desc" ) ),
+ scriptSetting
+ ),
+ Group.of(
+ get( "Preferences.r.delimiter.began" ),
+ Setting.of( label( "Preferences.r.delimiter.began.desc" ) ),
+ Setting.of( "Opening", mRDelimiterBegan )
+ ),
+ Group.of(
+ get( "Preferences.r.delimiter.ended" ),
+ Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ),
+ Setting.of( "Closing", mRDelimiterEnded )
+ )
+ ),
+ Category.of(
+ get( "Preferences.images" ),
+ Group.of(
+ get( "Preferences.images.directory" ),
+ Setting.of( label( "Preferences.images.directory.desc" ) ),
+ Setting.of( "Directory", mPropImagesDirectory, true )
+ ),
+ Group.of(
+ get( "Preferences.images.suffixes" ),
+ Setting.of( label( "Preferences.images.suffixes.desc" ) ),
+ Setting.of( "Extensions", mPropImagesOrder )
+ )
+ ),
+ Category.of(
+ get( "Preferences.definitions" ),
+ Group.of(
+ get( "Preferences.definitions.path" ),
+ Setting.of( label( "Preferences.definitions.path.desc" ) ),
+ Setting.of( "Path", mPropDefinitionPath, false )
+ ),
+ Group.of(
+ get( "Preferences.definitions.delimiter.began" ),
+ Setting.of( label(
+ "Preferences.definitions.delimiter.began.desc" ) ),
+ Setting.of( "Opening", mDefDelimiterBegan )
+ ),
+ Group.of(
+ get( "Preferences.definitions.delimiter.ended" ),
+ Setting.of( label(
+ "Preferences.definitions.delimiter.ended.desc" ) ),
+ Setting.of( "Closing", mDefDelimiterEnded )
+ )
+ ),
+ Category.of(
+ get( "Preferences.fonts" ),
+ Group.of(
+ get( "Preferences.fonts.size_editor" ),
+ Setting.of( label( "Preferences.fonts.size_editor.desc" ) ),
+ Setting.of( "Points", mPropFontsSizeEditor )
+ )
+ )
+ ).instantPersistent( false );
+ }
+
+ /**
+ * Wraps a {@link File} inside a {@link SimpleObjectProperty}.
+ *
+ * @param path The file name to use when constructing the {@link File}.
+ * @return A new {@link SimpleObjectProperty} instance with a {@link File}
+ * that references the given {@code path}.
+ */
+ private SimpleObjectProperty<File> simpleFile( final String path ) {
+ return new SimpleObjectProperty<>( new File( path ) );
+ }
+
+ /**
+ * Creates a label for the given key after interpolating its value.
+ *
+ * @param key The key to find in the resource bundle.
+ * @return The value of the key as a label.
+ */
+ private Node label( final String key ) {
+ return new Label( get( key, true ) );
+ }
+
+ /**
+ * Creates a label for the given key.
+ *
+ * @param key The key to find in the resource bundle.
+ * @param interpolate {@code true} means to interpolate the value.
+ * @return The value of the key, interpolated if {@code interpolate} is
+ * {@code true}.
+ */
+ @SuppressWarnings("SameParameterValue")
+ private Node label( final String key, final boolean interpolate ) {
+ return new Label( get( key, interpolate ) );
+ }
+
+ /**
+ * Delegates to the {@link PreferencesFx} event handler for monitoring
+ * save events.
+ *
+ * @param eventHandler The handler to call when the preferences are saved.
+ */
+ public void addSaveEventHandler(
+ final EventHandler<? super PreferencesFxEvent> eventHandler ) {
+ final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
+ getPreferencesFx().addEventHandler( eventType, eventHandler );
+ }
+
+ /**
+ * Returns the value for a key from the settings properties file.
+ *
+ * @param key Key within the settings properties file to find.
+ * @param value Default value to return if the key is not found.
+ * @return The value for the given key from the settings file, or the
+ * given {@code value} if no key found.
+ */
+ @SuppressWarnings("SameParameterValue")
+ private String getSetting( final String key, final String value ) {
+ return SETTINGS.getSetting( key, value );
+ }
+
+ public ObjectProperty<File> definitionPathProperty() {
+ return mPropDefinitionPath;
+ }
+
+ public Path getDefinitionPath() {
+ return definitionPathProperty().getValue().toPath();
+ }
+
+ private StringProperty defDelimiterBegan() {
+ return mDefDelimiterBegan;
+ }
+
+ public String getDefDelimiterBegan() {
+ return defDelimiterBegan().get();
+ }
+
+ private StringProperty defDelimiterEnded() {
+ return mDefDelimiterEnded;
+ }
+
+ public String getDefDelimiterEnded() {
+ return defDelimiterEnded().get();
+ }
+
+ public ObjectProperty<File> rDirectoryProperty() {
+ return mPropRDirectory;
+ }
+
+ public File getRDirectory() {
+ return rDirectoryProperty().getValue();
+ }
+
+ public StringProperty rScriptProperty() {
+ return mPropRScript;
+ }
+
+ public String getRScript() {
+ return rScriptProperty().getValue();
+ }
+
+ private StringProperty rDelimiterBegan() {
+ return mRDelimiterBegan;
+ }
+
+ public String getRDelimiterBegan() {
+ return rDelimiterBegan().get();
+ }
+
+ private StringProperty rDelimiterEnded() {
+ return mRDelimiterEnded;
+ }
+
+ public String getRDelimiterEnded() {
+ return rDelimiterEnded().get();
+ }
+
+ private ObjectProperty<File> imagesDirectoryProperty() {
+ return mPropImagesDirectory;
+ }
+
+ public File getImagesDirectory() {
+ return imagesDirectoryProperty().getValue();
+ }
+
+ private StringProperty imagesOrderProperty() {
+ return mPropImagesOrder;
+ }
+
+ public String getImagesOrder() {
+ return imagesOrderProperty().getValue();
+ }
+
+ public IntegerProperty fontsSizeEditorProperty() {
+ return mPropFontsSizeEditor;
+ }
+
+ /**
+ * Returns the preferred font size of the text editor.
+ *
+ * @return A non-negative integer, in points.
+ */
+ public int getFontsSizeEditor() {
+ return mPropFontsSizeEditor.intValue();
+ }
+
+ private PreferencesFx getPreferencesFx() {
+ return mPreferencesFx;
+ }
+}
src/main/java/com/keenwrite/preview/ChainedReplacedElementFactory.java
+/*
+ * Copyright 2006 Patrick Wright
+ * Copyright 2007 Wisconsin Court System
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+package com.keenwrite.preview;
+
+import com.keenwrite.adapters.ReplacedElementAdapter;
+import org.w3c.dom.Element;
+import org.xhtmlrenderer.extend.ReplacedElement;
+import org.xhtmlrenderer.extend.ReplacedElementFactory;
+import org.xhtmlrenderer.extend.UserAgentCallback;
+import org.xhtmlrenderer.layout.LayoutContext;
+import org.xhtmlrenderer.render.BlockBox;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class ChainedReplacedElementFactory extends ReplacedElementAdapter {
+ private final Set<ReplacedElementFactory> mFactoryList = new HashSet<>();
+
+ @Override
+ public ReplacedElement createReplacedElement(
+ final LayoutContext c,
+ final BlockBox box,
+ final UserAgentCallback uac,
+ final int cssWidth,
+ final int cssHeight ) {
+ for( final var f : mFactoryList ) {
+ final var r = f.createReplacedElement(
+ c, box, uac, cssWidth, cssHeight );
+
+ if( r != null ) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void reset() {
+ for( final var factory : mFactoryList ) {
+ factory.reset();
+ }
+ }
+
+ @Override
+ public void remove( final Element element ) {
+ for( final var factory : mFactoryList ) {
+ factory.remove( element );
+ }
+ }
+
+ public void addFactory( final ReplacedElementFactory factory ) {
+ mFactoryList.add( factory );
+ }
+}
src/main/java/com/keenwrite/preview/CustomImageLoader.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preview;
+
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+import org.xhtmlrenderer.extend.FSImage;
+import org.xhtmlrenderer.resource.ImageResource;
+import org.xhtmlrenderer.swing.ImageResourceLoader;
+
+import javax.imageio.ImageIO;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Paths;
+
+import static com.keenwrite.StatusBarNotifier.alert;
+import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
+import static com.keenwrite.util.ProtocolResolver.getProtocol;
+import static java.lang.String.valueOf;
+import static java.nio.file.Files.exists;
+import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
+
+/**
+ * Responsible for loading images. If the image cannot be found, a placeholder
+ * is used instead.
+ */
+public class CustomImageLoader extends ImageResourceLoader {
+ /**
+ * Placeholder that's displayed when image cannot be found.
+ */
+ private FSImage mBrokenImage;
+
+ private final IntegerProperty mWidthProperty = new SimpleIntegerProperty();
+
+ /**
+ * Gets an {@link IntegerProperty} that represents the maximum width an
+ * image should be scaled.
+ *
+ * @return The maximum width for an image.
+ */
+ public IntegerProperty widthProperty() {
+ return mWidthProperty;
+ }
+
+ /**
+ * Gets an image resolved from the given URI. If the image cannot be found,
+ * this will return a custom placeholder image indicating the reference
+ * is broken.
+ *
+ * @param uri Path to the image resource to load.
+ * @param width Ignored.
+ * @param height Ignored.
+ * @return The scaled image, or a placeholder image if the URI's content
+ * could not be retrieved.
+ */
+ @Override
+ public synchronized ImageResource get(
+ final String uri, final int width, final int height ) {
+ assert uri != null;
+ assert width >= 0;
+ assert height >= 0;
+
+ try {
+ final var protocol = getProtocol( uri );
+ final ImageResource imageResource;
+
+ if( protocol.isFile() && exists( Paths.get( new URI( uri ) ) ) ) {
+ imageResource = super.get( uri, width, height );
+ }
+ else if( protocol.isHttp() ) {
+ // FlyingSaucer will silently swallow any images that fail to load.
+ // Consequently, the following lines load the resource over HTTP and
+ // translate errors into a broken image icon.
+ final var url = new URL( uri );
+ final var image = ImageIO.read( url );
+ imageResource = new ImageResource( uri, createImage( image ) );
+ }
+ else {
+ // Caught below to return a broken image; exception is swallowed.
+ throw new UnsupportedOperationException( valueOf( protocol ) );
+ }
+
+ return scale( imageResource );
+ } catch( final Exception e ) {
+ alert( e );
+ return new ImageResource( uri, getBrokenImage() );
+ }
+ }
+
+ /**
+ * Scales the image found at the given URI.
+ *
+ * @param ir {@link ImageResource} of image loaded successfully.
+ * @return Resource representing the rendered image and path.
+ */
+ private ImageResource scale( final ImageResource ir ) {
+ final var image = ir.getImage();
+ final var imageWidth = image.getWidth();
+ final var imageHeight = image.getHeight();
+
+ int maxWidth = mWidthProperty.get();
+ int newWidth = imageWidth;
+ int newHeight = imageHeight;
+
+ // Maintain aspect ratio while shrinking image to view port bounds.
+ if( imageWidth > maxWidth ) {
+ newWidth = maxWidth;
+ newHeight = (newWidth * imageHeight) / imageWidth;
+ }
+
+ image.scale( newWidth, newHeight );
+ return ir;
+ }
+
+ /**
+ * Lazily initializes the broken image placeholder.
+ *
+ * @return The {@link FSImage} that represents a broken image icon.
+ */
+ private FSImage getBrokenImage() {
+ final var image = mBrokenImage;
+
+ if( image == null ) {
+ mBrokenImage = createImage( BROKEN_IMAGE_PLACEHOLDER );
+ }
+
+ return mBrokenImage;
+ }
+}
src/main/java/com/keenwrite/preview/HTMLPreviewPane.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preview;
+
+import com.keenwrite.adapters.DocumentAdapter;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.embed.swing.SwingNode;
+import javafx.scene.Node;
+import org.jsoup.Jsoup;
+import org.jsoup.helper.W3CDom;
+import org.jsoup.nodes.Document;
+import org.xhtmlrenderer.layout.SharedContext;
+import org.xhtmlrenderer.render.Box;
+import org.xhtmlrenderer.simple.XHTMLPanel;
+import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
+import org.xhtmlrenderer.swing.*;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.net.URI;
+import java.nio.file.Path;
+
+import static com.keenwrite.Constants.*;
+import static com.keenwrite.StatusBarNotifier.alert;
+import static com.keenwrite.util.ProtocolResolver.getProtocol;
+import static java.awt.Desktop.Action.BROWSE;
+import static java.awt.Desktop.getDesktop;
+import static java.lang.Math.max;
+import static javax.swing.SwingUtilities.invokeLater;
+import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
+
+/**
+ * HTML preview pane is responsible for rendering an HTML document.
+ */
+public final class HTMLPreviewPane extends SwingNode {
+
+ /**
+ * Suppresses scrolling to the top on every key press.
+ */
+ private static class HTMLPanel extends XHTMLPanel {
+ @Override
+ public void resetScrollPosition() {
+ }
+ }
+
+ /**
+ * Suppresses scroll attempts until after the document has loaded.
+ */
+ private static final class DocumentEventHandler extends DocumentAdapter {
+ private final BooleanProperty mReadyProperty = new SimpleBooleanProperty();
+
+ public BooleanProperty readyProperty() {
+ return mReadyProperty;
+ }
+
+ @Override
+ public void documentStarted() {
+ mReadyProperty.setValue( Boolean.FALSE );
+ }
+
+ @Override
+ public void documentLoaded() {
+ mReadyProperty.setValue( Boolean.TRUE );
+ }
+ }
+
+ /**
+ * Ensure that images are constrained to the panel width upon resizing.
+ */
+ private final class ResizeListener extends ComponentAdapter {
+ @Override
+ public void componentResized( final ComponentEvent e ) {
+ setWidth( e );
+ }
+
+ @Override
+ public void componentShown( final ComponentEvent e ) {
+ setWidth( e );
+ }
+
+ /**
+ * Sets the width of the {@link HTMLPreviewPane} so that images can be
+ * scaled to fit. The scale factor is adjusted a bit below the full width
+ * to prevent the horizontal scrollbar from appearing.
+ *
+ * @param event The component that defines the image scaling width.
+ */
+ private void setWidth( final ComponentEvent event ) {
+ final int width = (int) (event.getComponent().getWidth() * .95);
+ HTMLPreviewPane.this.mImageLoader.widthProperty().set( width );
+ }
+ }
+
+ /**
+ * Responsible for opening hyperlinks. External hyperlinks are opened in
+ * the system's default browser; local file system links are opened in the
+ * editor.
+ */
+ private static class HyperlinkListener extends LinkListener {
+ @Override
+ public void linkClicked( final BasicPanel panel, final String link ) {
+ try {
+ final var protocol = getProtocol( link );
+
+ switch( protocol ) {
+ case HTTP:
+ final var desktop = getDesktop();
+
+ if( desktop.isSupported( BROWSE ) ) {
+ desktop.browse( new URI( link ) );
+ }
+ break;
+ case FILE:
+ // TODO: #88 -- publish a message to the event bus.
+ break;
+ }
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+ }
+
+ /**
+ * The CSS must be rendered in points (pt) not pixels (px) to avoid blurry
+ * rendering on some platforms.
+ */
+ private static final String HTML_PREFIX = "<!DOCTYPE html>"
+ + "<html>"
+ + "<head>"
+ + "<link rel='stylesheet' href='" +
+ HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) + "'/>"
+ + "</head>"
+ + "<body>";
+
+ // Provide some extra space at the end for scrolling past the last line.
+ private static final String HTML_SUFFIX =
+ "<p style='height=2em'>&nbsp;</p></body></html>";
+
+ private static final W3CDom W3C_DOM = new W3CDom();
+ private static final XhtmlNamespaceHandler NS_HANDLER =
+ new XhtmlNamespaceHandler();
+
+ private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
+ private final int mHtmlPrefixLength;
+
+ private final HTMLPanel mHtmlRenderer = new HTMLPanel();
+ private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
+ private final DocumentEventHandler mDocHandler = new DocumentEventHandler();
+ private final CustomImageLoader mImageLoader = new CustomImageLoader();
+
+ private Path mPath = DEFAULT_DIRECTORY;
+
+ /**
+ * Creates a new preview pane that can scroll to the caret position within the
+ * document.
+ */
+ public HTMLPreviewPane() {
+ setStyle( "-fx-background-color: white;" );
+
+ // No need to append same prefix each time the HTML content is updated.
+ mHtmlDocument.append( HTML_PREFIX );
+ mHtmlPrefixLength = mHtmlDocument.length();
+
+ // Inject an SVG renderer that produces high-quality SVG buffered images.
+ final var factory = new ChainedReplacedElementFactory();
+ factory.addFactory( new SvgReplacedElementFactory() );
+ factory.addFactory( new SwingReplacedElementFactory(
+ NO_OP_REPAINT_LISTENER, mImageLoader ) );
+
+ final var context = getSharedContext();
+ final var textRenderer = context.getTextRenderer();
+ context.setReplacedElementFactory( factory );
+ textRenderer.setSmoothingThreshold( 0 );
+
+ setContent( mScrollPane );
+ mHtmlRenderer.addDocumentListener( mDocHandler );
+ mHtmlRenderer.addComponentListener( new ResizeListener() );
+
+ // The default mouse click listener attempts navigation within the
+ // preview panel. We want to usurp that behaviour to open the link in
+ // a platform-specific browser.
+ for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) {
+ if( !(listener instanceof HoverListener) ) {
+ mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener );
+ }
+ }
+
+ mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() );
+ }
+
+ /**
+ * Updates the internal HTML source, loads it into the preview pane, then
+ * scrolls to the caret position.
+ *
+ * @param html The new HTML document to display.
+ */
+ public void process( final String html ) {
+ final Document jsoupDoc = Jsoup.parse( decorate( html ) );
+ final org.w3c.dom.Document w3cDoc = W3C_DOM.fromJsoup( jsoupDoc );
+
+
+ // Access to a Swing component must occur from the Event Dispatch
+ // thread according to Swing threading restrictions.
+ invokeLater(
+ () -> mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), NS_HANDLER )
+ );
+ }
+
+ public void clear() {
+ process( "" );
+ }
+
+ /**
+ * Scrolls to an anchor link. The anchor links are injected when the
+ * HTML document is created.
+ *
+ * @param id The unique anchor link identifier.
+ */
+ public void tryScrollTo( final int id ) {
+ final ChangeListener<Boolean> listener = new ChangeListener<>() {
+ @Override
+ public void changed(
+ final ObservableValue<? extends Boolean> observable,
+ final Boolean oldValue,
+ final Boolean newValue ) {
+ if( newValue ) {
+ scrollTo( id );
+
+ mDocHandler.readyProperty().removeListener( this );
+ }
+ }
+ };
+
+ mDocHandler.readyProperty().addListener( listener );
+ }
+
+ /**
+ * Scrolls to the closest element matching the given identifier without
+ * waiting for the document to be ready. Be sure the document is ready
+ * before calling this method.
+ *
+ * @param id Paragraph index.
+ */
+ public void scrollTo( final int id ) {
+ if( id < 2 ) {
+ scrollToTop();
+ }
+ else {
+ Box box = findPrevBox( id );
+ box = box == null ? findNextBox( id + 1 ) : box;
+
+ if( box == null ) {
+ scrollToBottom();
+ }
+ else {
+ scrollTo( box );
+ }
+ }
+ }
+
+ private Box findPrevBox( final int id ) {
+ int prevId = id;
+ Box box = null;
+
+ while( prevId > 0 && (box = getBoxById( PARAGRAPH_ID_PREFIX + prevId )) == null ) {
+ prevId--;
+ }
+
+ return box;
+ }
+
+ private Box findNextBox( final int id ) {
+ int nextId = id;
+ Box box = null;
+
+ while( nextId - id < 5 &&
+ (box = getBoxById( PARAGRAPH_ID_PREFIX + nextId )) == null ) {
+ nextId++;
+ }
+
+ return box;
+ }
+
+ private void scrollTo( final Point point ) {
+ invokeLater( () -> mHtmlRenderer.scrollTo( point ) );
+ }
+
+ private void scrollTo( final Box box ) {
+ scrollTo( createPoint( box ) );
+ }
+
+ private void scrollToY( final int y ) {
+ scrollTo( new Point( 0, y ) );
+ }
+
+ private void scrollToTop() {
+ scrollToY( 0 );
+ }
+
+ private void scrollToBottom() {
+ scrollToY( mHtmlRenderer.getHeight() );
+ }
+
+ private Box getBoxById( final String id ) {
+ return getSharedContext().getBoxById( id );
+ }
+
+ private String decorate( final String html ) {
+ // Trim the HTML back to only the prefix.
+ mHtmlDocument.setLength( mHtmlPrefixLength );
+
+ // Write the HTML body element followed by closing tags.
+ return mHtmlDocument.append( html ).append( HTML_SUFFIX ).toString();
+ }
+
+ public Path getPath() {
+ return mPath;
+ }
+
+ public void setPath( final Path path ) {
+ assert path != null;
+ mPath = path;
+ }
+
+ /**
+ * Content to embed in a panel.
+ *
+ * @return The content to display to the user.
+ */
+ public Node getNode() {
+ return this;
+ }
+
+ public JScrollPane getScrollPane() {
+ return mScrollPane;
+ }
+
+ public JScrollBar getVerticalScrollBar() {
+ return getScrollPane().getVerticalScrollBar();
+ }
+
+ /**
+ * Creates a {@link Point} to use as a reference for scrolling to the area
+ * described by the given {@link Box}. The {@link Box} coordinates are used
+ * to populate the {@link Point}'s location, with minor adjustments for
+ * vertical centering.
+ *
+ * @param box The {@link Box} that represents a scrolling anchor reference.
+ * @return A coordinate suitable for scrolling to.
+ */
+ private Point createPoint( final Box box ) {
+ assert box != null;
+
+ int x = box.getAbsX();
+
+ // Scroll back up by half the height of the scroll bar to keep the typing
+ // area within the view port. Otherwise the view port will have jumped too
+ // high up and the whatever gets typed won't be visible.
+ int y = max(
+ box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2),
+ 0 );
+
+ if( !box.getStyle().isInline() ) {
+ final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() );
+ x += margin.left();
+ y += margin.top();
+ }
+
+ return new Point( x, y );
+ }
+
+ private String getBaseUrl() {
+ final Path basePath = getPath();
+ final Path parent = basePath == null ? null : basePath.getParent();
+
+ return parent == null ? "" : parent.toUri().toString();
+ }
+
+ private SharedContext getSharedContext() {
+ return mHtmlRenderer.getSharedContext();
+ }
+}
src/main/java/com/keenwrite/preview/MathRenderer.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preview;
+
+import com.whitemagicsoftware.tex.*;
+import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
+import org.w3c.dom.Document;
+
+import java.util.function.Supplier;
+
+import static com.keenwrite.StatusBarNotifier.alert;
+
+/**
+ * Responsible for rendering formulas as scalable vector graphics (SVG).
+ */
+public class MathRenderer {
+
+ /**
+ * Default font size in points.
+ */
+ private static final float FONT_SIZE = 20f;
+
+ private final TeXFont mTeXFont = createDefaultTeXFont( FONT_SIZE );
+ private final TeXEnvironment mEnvironment = createTeXEnvironment( mTeXFont );
+ private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D();
+
+ public MathRenderer() {
+ mGraphics.scale( FONT_SIZE, FONT_SIZE );
+ }
+
+ /**
+ * This method only takes a few seconds to generate
+ *
+ * @param equation A mathematical expression to render.
+ * @return The given string with all formulas transformed into SVG format.
+ */
+ public Document render( final String equation ) {
+ final var formula = new TeXFormula( equation );
+ final var box = formula.createBox( mEnvironment );
+ final var l = new TeXLayout( box, FONT_SIZE );
+
+ mGraphics.initialize( l.getWidth(), l.getHeight() );
+ box.draw( mGraphics, l.getX(), l.getY() );
+ return mGraphics.toDom();
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private TeXFont createDefaultTeXFont( final float fontSize ) {
+ return create( () -> new DefaultTeXFont( fontSize ) );
+ }
+
+ private TeXEnvironment createTeXEnvironment( final TeXFont texFont ) {
+ return create( () -> new TeXEnvironment( texFont ) );
+ }
+
+ private SvgDomGraphics2D createSvgDomGraphics2D() {
+ return create( SvgDomGraphics2D::new );
+ }
+
+ /**
+ * Tries to instantiate a given object, returning {@code null} on failure.
+ * The failure message is bubbled up to to the user interface.
+ *
+ * @param supplier Creates an instance.
+ * @param <T> The type of instance being created.
+ * @return An instance of the parameterized type or {@code null} upon error.
+ */
+ private <T> T create( final Supplier<T> supplier ) {
+ try {
+ return supplier.get();
+ } catch( final Exception ex ) {
+ alert( ex );
+ return null;
+ }
+ }
+}
src/main/java/com/keenwrite/preview/RenderingSettings.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preview;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static java.awt.RenderingHints.*;
+import static java.awt.Toolkit.getDefaultToolkit;
+
+/**
+ * Responsible for supplying consistent rendering hints throughout the
+ * application, such as image rendering for {@link SvgRasterizer}.
+ */
+@SuppressWarnings("rawtypes")
+public class RenderingSettings {
+
+ /**
+ * Default hints for high-quality rendering that may be changed by
+ * the system's rendering hints.
+ */
+ private static final Map<Object, Object> DEFAULT_HINTS = Map.of(
+ KEY_ANTIALIASING,
+ VALUE_ANTIALIAS_ON,
+ KEY_ALPHA_INTERPOLATION,
+ VALUE_ALPHA_INTERPOLATION_QUALITY,
+ KEY_COLOR_RENDERING,
+ VALUE_COLOR_RENDER_QUALITY,
+ KEY_DITHERING,
+ VALUE_DITHER_DISABLE,
+ KEY_FRACTIONALMETRICS,
+ VALUE_FRACTIONALMETRICS_ON,
+ KEY_INTERPOLATION,
+ VALUE_INTERPOLATION_BICUBIC,
+ KEY_RENDERING,
+ VALUE_RENDER_QUALITY,
+ KEY_STROKE_CONTROL,
+ VALUE_STROKE_PURE,
+ KEY_TEXT_ANTIALIASING,
+ VALUE_TEXT_ANTIALIAS_ON
+ );
+
+ /**
+ * Shared hints for high-quality rendering.
+ */
+ public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>(
+ DEFAULT_HINTS
+ );
+
+ static {
+ final var toolkit = getDefaultToolkit();
+ final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" );
+
+ if( hints instanceof Map ) {
+ final var map = (Map) hints;
+ for( final var key : map.keySet() ) {
+ final var hint = map.get( key );
+ RENDERING_HINTS.put( key, hint );
+ }
+ }
+ }
+
+ /**
+ * Prevent instantiation as per Joshua Bloch's recommendation.
+ */
+ private RenderingSettings() {
+ }
+}
src/main/java/com/keenwrite/preview/SvgRasterizer.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preview;
+
+import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
+import org.apache.batik.gvt.renderer.ImageRenderer;
+import org.apache.batik.transcoder.TranscoderException;
+import org.apache.batik.transcoder.TranscoderInput;
+import org.apache.batik.transcoder.TranscoderOutput;
+import org.apache.batik.transcoder.image.ImageTranscoder;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.URL;
+import java.text.NumberFormat;
+
+import static com.keenwrite.StatusBarNotifier.alert;
+import static com.keenwrite.preview.RenderingSettings.RENDERING_HINTS;
+import static java.awt.image.BufferedImage.TYPE_INT_RGB;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.text.NumberFormat.getIntegerInstance;
+import static javax.xml.transform.OutputKeys.*;
+import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
+import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
+
+/**
+ * Responsible for converting SVG images into rasterized PNG images.
+ */
+public class SvgRasterizer {
+ private static final SAXSVGDocumentFactory FACTORY_DOM =
+ new SAXSVGDocumentFactory( getXMLParserClassName() );
+
+ private static final TransformerFactory FACTORY_TRANSFORM =
+ TransformerFactory.newInstance();
+
+ private static final Transformer sTransformer;
+
+ static {
+ Transformer t;
+
+ try {
+ t = FACTORY_TRANSFORM.newTransformer();
+ t.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
+ t.setOutputProperty( METHOD, "xml" );
+ t.setOutputProperty( INDENT, "no" );
+ t.setOutputProperty( ENCODING, UTF_8.name() );
+ } catch( final TransformerConfigurationException e ) {
+ t = null;
+ }
+
+ sTransformer = t;
+ }
+
+ private static final NumberFormat INT_FORMAT = getIntegerInstance();
+
+ public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER;
+
+ /**
+ * A FontAwesome camera icon, cleft asunder.
+ */
+ public static final String BROKEN_IMAGE_SVG =
+ "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" +
+ ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" +
+ ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " +
+ "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" +
+ ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" +
+ ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" +
+ ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" +
+ ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " +
+ "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" +
+ ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" +
+ ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" +
+ ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" +
+ ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" +
+ ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" +
+ ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" +
+ ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" +
+ ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " +
+ ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" +
+ ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" +
+ ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" +
+ ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" +
+ ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" +
+ ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" +
+ ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " +
+ "0'/></g></svg>";
+
+ static {
+ // The width and height cannot be embedded in the SVG above because the
+ // path element values are relative to the viewBox dimensions.
+ final int w = 75;
+ final int h = 75;
+ BufferedImage image;
+
+ try {
+ image = rasterizeString( BROKEN_IMAGE_SVG, w );
+ } catch( final Exception e ) {
+ image = new BufferedImage( w, h, TYPE_INT_RGB );
+ final var graphics = (Graphics2D) image.getGraphics();
+ graphics.setRenderingHints( RENDERING_HINTS );
+
+ // Fall back to a (\) symbol.
+ graphics.setColor( new Color( 204, 204, 204 ) );
+ graphics.fillRect( 0, 0, w, h );
+ graphics.setColor( new Color( 255, 204, 204 ) );
+ graphics.setStroke( new BasicStroke( 4 ) );
+ graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
+ graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
+ h / 4 + (int) (w / 4 / Math.PI),
+ w / 2 + w / 4 - (int) (w / 4 / Math.PI),
+ h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
+ }
+
+ BROKEN_IMAGE_PLACEHOLDER = image;
+ }
+
+ /**
+ * Responsible for creating a new {@link ImageRenderer} implementation that
+ * can render a DOM as an SVG image.
+ */
+ private static class BufferedImageTranscoder extends ImageTranscoder {
+ private BufferedImage mImage;
+
+ @Override
+ public BufferedImage createImage( final int w, final int h ) {
+ return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
+ }
+
+ @Override
+ public void writeImage(
+ final BufferedImage image, final TranscoderOutput output ) {
+ mImage = image;
+ }
+
+ public BufferedImage getImage() {
+ return mImage;
+ }
+
+ @Override
+ protected ImageRenderer createRenderer() {
+ final ImageRenderer renderer = super.createRenderer();
+ final RenderingHints hints = renderer.getRenderingHints();
+ hints.putAll( RENDERING_HINTS );
+
+ renderer.setRenderingHints( hints );
+
+ return renderer;
+ }
+ }
+
+ /**
+ * Rasterizes the vector graphic file at the given URL. If any exception
+ * happens, a red circle is returned instead.
+ *
+ * @param url The URL to a vector graphic file, which must include the
+ * protocol scheme (such as file:// or https://).
+ * @param width The number of pixels wide to render the image. The aspect
+ * ratio is maintained.
+ * @return Either the rasterized image upon success or a red circle.
+ */
+ public static BufferedImage rasterize( final String url, final int width ) {
+ try {
+ return rasterize( new URL( url ), width );
+ } catch( final Exception ex ) {
+ alert( ex );
+ return BROKEN_IMAGE_PLACEHOLDER;
+ }
+ }
+
+ /**
+ * Rasterizes the given document into an image.
+ *
+ * @param svg The SVG {@link Document} to rasterize.
+ * @param width The rasterized image's width (in pixels).
+ * @return The rasterized image.
+ * @throws TranscoderException Signifies an issue with the input document.
+ */
+ public static BufferedImage rasterize( final Document svg, final int width )
+ throws TranscoderException {
+ final var transcoder = new BufferedImageTranscoder();
+ final var input = new TranscoderInput( svg );
+
+ transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
+ transcoder.transcode( input, null );
+
+ return transcoder.getImage();
+ }
+
+ /**
+ * Converts an SVG drawing into a rasterized image that can be drawn on
+ * a graphics context.
+ *
+ * @param url The path to the image (can be web address).
+ * @param width Scale the image width to this size (aspect ratio is
+ * maintained).
+ * @return The vector graphic transcoded into a raster image format.
+ * @throws IOException Could not read the vector graphic.
+ * @throws TranscoderException Could not convert the vector graphic to an
+ * instance of {@link Image}.
+ */
+ public static BufferedImage rasterize( final URL url, final int width )
+ throws IOException, TranscoderException {
+ return rasterize( FACTORY_DOM.createDocument( url.toString() ), width );
+ }
+
+ public static BufferedImage rasterize( final Document document ) {
+ try {
+ final var root = document.getDocumentElement();
+ final var width = root.getAttribute( "width" );
+ return rasterize( document, INT_FORMAT.parse( width ).intValue() );
+ } catch( final Exception ex ) {
+ alert( ex );
+ return BROKEN_IMAGE_PLACEHOLDER;
+ }
+ }
+
+ /**
+ * Converts an SVG string into a rasterized image that can be drawn on
+ * a graphics context.
+ *
+ * @param svg The SVG xml document.
+ * @param w Scale the image width to this size (aspect ratio is
+ * maintained).
+ * @return The vector graphic transcoded into a raster image format.
+ * @throws TranscoderException Could not convert the vector graphic to an
+ * instance of {@link Image}.
+ */
+ public static BufferedImage rasterizeString( final String svg, final int w )
+ throws IOException, TranscoderException {
+ return rasterize( toDocument( svg ), w );
+ }
+
+ /**
+ * Converts an SVG string into a rasterized image that can be drawn on
+ * a graphics context. The dimensions are determined from the document.
+ *
+ * @param xml The SVG xml document.
+ * @return The vector graphic transcoded into a raster image format.
+ */
+ public static BufferedImage rasterizeString( final String xml ) {
+ try {
+ final var document = toDocument( xml );
+ final var root = document.getDocumentElement();
+ final var width = root.getAttribute( "width" );
+ return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() );
+ } catch( final Exception ex ) {
+ alert( ex );
+ return BROKEN_IMAGE_PLACEHOLDER;
+ }
+ }
+
+ /**
+ * Converts an SVG XML string into a new {@link Document} instance.
+ *
+ * @param xml The XML containing SVG elements.
+ * @return The SVG contents parsed into a {@link Document} object model.
+ * @throws IOException Could
+ */
+ private static Document toDocument( final String xml ) throws IOException {
+ try( final var reader = new StringReader( xml ) ) {
+ return FACTORY_DOM.createSVGDocument(
+ "http://www.w3.org/2000/svg", reader );
+ }
+ }
+
+ /**
+ * Given a document object model (DOM) {@link Element}, this will convert that
+ * element to a string.
+ *
+ * @param e The DOM node to convert to a string.
+ * @return The DOM node as an escaped, plain text string.
+ */
+ public static String toSvg( final Element e ) {
+ try( final var writer = new StringWriter() ) {
+ sTransformer.transform( new DOMSource( e ), new StreamResult( writer ) );
+ return writer.toString().replaceAll( "xmlns=\"\" ", "" );
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+
+ return BROKEN_IMAGE_SVG;
+ }
+}
src/main/java/com/keenwrite/preview/SvgReplacedElementFactory.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.preview;
+
+import com.keenwrite.util.BoundedCache;
+import org.apache.commons.io.FilenameUtils;
+import org.w3c.dom.Element;
+import org.xhtmlrenderer.extend.ReplacedElement;
+import org.xhtmlrenderer.extend.ReplacedElementFactory;
+import org.xhtmlrenderer.extend.UserAgentCallback;
+import org.xhtmlrenderer.layout.LayoutContext;
+import org.xhtmlrenderer.render.BlockBox;
+import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
+import org.xhtmlrenderer.swing.ImageReplacedElement;
+
+import java.awt.image.BufferedImage;
+import java.util.Map;
+import java.util.function.Function;
+
+import static com.keenwrite.StatusBarNotifier.alert;
+import static com.keenwrite.preview.SvgRasterizer.rasterize;
+import static com.keenwrite.processors.markdown.tex.TeXNode.HTML_TEX;
+
+/**
+ * Responsible for running {@link SvgRasterizer} on SVG images detected within
+ * a document to transform them into rasterized versions.
+ */
+public class SvgReplacedElementFactory implements ReplacedElementFactory {
+
+ /**
+ * Prevent instantiation until needed.
+ */
+ private static class MathRendererContainer {
+ private static final MathRenderer INSTANCE = new MathRenderer();
+ }
+
+ /**
+ * Returns the singleton instance for rendering math symbols.
+ *
+ * @return A non-null instance, loaded, configured, and ready to render math.
+ */
+ public static MathRenderer getInstance() {
+ return MathRendererContainer.INSTANCE;
+ }
+
+ /**
+ * SVG filename extension maps to an SVG image element.
+ */
+ private static final String SVG_FILE = "svg";
+
+ private static final String HTML_IMAGE = "img";
+ private static final String HTML_IMAGE_SRC = "src";
+
+ /**
+ * A bounded cache that removes the oldest image if the maximum number of
+ * cached images has been reached. This constrains the number of images
+ * loaded into memory.
+ */
+ private final Map<String, BufferedImage> mImageCache =
+ new BoundedCache<>( 150 );
+
+ @Override
+ public ReplacedElement createReplacedElement(
+ final LayoutContext c,
+ final BlockBox box,
+ final UserAgentCallback uac,
+ final int cssWidth,
+ final int cssHeight ) {
+ BufferedImage image = null;
+ final var e = box.getElement();
+
+ if( e != null ) {
+ try {
+ final var nodeName = e.getNodeName();
+
+ if( HTML_IMAGE.equals( nodeName ) ) {
+ final var src = e.getAttribute( HTML_IMAGE_SRC );
+ final var ext = FilenameUtils.getExtension( src );
+
+ if( SVG_FILE.equalsIgnoreCase( ext ) ) {
+ image = getCachedImage(
+ src, svg -> rasterize( svg, box.getContentWidth() ) );
+ }
+ }
+ else if( HTML_TEX.equals( nodeName ) ) {
+ // Convert the TeX element to a raster graphic if not yet cached.
+ final var src = e.getTextContent();
+ image = getCachedImage(
+ src, __ -> rasterize( getInstance().render( src ) )
+ );
+ }
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+ }
+
+ if( image != null ) {
+ final var w = image.getWidth( null );
+ final var h = image.getHeight( null );
+
+ return new ImageReplacedElement( image, w, h );
+ }
+
+ return null;
+ }
+
+ @Override
+ public void reset() {
+ }
+
+ @Override
+ public void remove( final Element e ) {
+ }
+
+ @Override
+ public void setFormSubmissionListener( FormSubmissionListener listener ) {
+ }
+
+ /**
+ * Returns an image associated with a string; the string's pre-computed
+ * hash code is returned as the string value, making this operation very
+ * quick to return the corresponding {@link BufferedImage}.
+ *
+ * @param src The source used for the key into the image cache.
+ * @param rasterizer {@link Function} to call to rasterize an image.
+ * @return The image that corresponds to the given source string.
+ */
+ private BufferedImage getCachedImage(
+ final String src, final Function<String, BufferedImage> rasterizer ) {
+ return mImageCache.computeIfAbsent( src, __ -> rasterizer.apply( src ) );
+ }
+}
src/main/java/com/keenwrite/processors/AbstractProcessor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+/**
+ * Responsible for transforming a document through a variety of chained
+ * handlers. If there are conditions where this handler should not process the
+ * entire chain, create a second handler, or split the chain into reusable
+ * sub-chains.
+ *
+ * @param <T> The type of object to process.
+ */
+public abstract class AbstractProcessor<T> implements Processor<T> {
+
+ /**
+ * Used while processing the entire chain; null to signify no more links.
+ */
+ private final Processor<T> mNext;
+
+ /**
+ * Constructs a new default handler with no successor.
+ */
+ protected AbstractProcessor() {
+ this( null );
+ }
+
+ /**
+ * Constructs a new default handler with a given successor.
+ *
+ * @param successor The next processor in the chain.
+ */
+ public AbstractProcessor( final Processor<T> successor ) {
+ mNext = successor;
+ }
+
+ @Override
+ public Processor<T> next() {
+ return mNext;
+ }
+
+ /**
+ * This algorithm is incorrect, but works for the one use case of removing
+ * the ending HTML Preview Processor from the end of the processor chain.
+ * The processor chain is immutable so this creates a succession of
+ * delegators that wrap each processor in the chain, except for the one
+ * to be removed.
+ * <p>
+ * An alternative is to update the {@link ProcessorFactory} with the ability
+ * to create a processor chain devoid of an {@link HtmlPreviewProcessor}.
+ * </p>
+ *
+ * @param removal The {@link Processor} to remove from the chain.
+ * @return A delegating processor chain starting from this processor
+ * onwards with the given processor removed from the chain.
+ */
+ @Override
+ public Processor<T> remove( final Class<? extends Processor<T>> removal ) {
+ Processor<T> p = this;
+ final ProcessorDelegator<T> head = new ProcessorDelegator<>( p );
+ ProcessorDelegator<T> result = head;
+
+ while( p != null ) {
+ final Processor<T> next = p.next();
+
+ if( next != null && next.getClass() != removal ) {
+ final var delegator = new ProcessorDelegator<>( next );
+
+ result.setNext( delegator );
+ result = delegator;
+ }
+
+ p = p.next();
+ }
+
+ return head;
+ }
+
+ private static final class ProcessorDelegator<T>
+ extends AbstractProcessor<T> {
+ private final Processor<T> mDelegate;
+ private Processor<T> mNext;
+
+ public ProcessorDelegator( final Processor<T> delegate ) {
+ super( delegate );
+
+ assert delegate != null;
+
+ mDelegate = delegate;
+ }
+
+ @Override
+ public T apply( T t ) {
+ return mDelegate.apply( t );
+ }
+
+ protected void setNext( final Processor<T> next ) {
+ mNext = next;
+ }
+
+ @Override
+ public Processor<T> next() {
+ return mNext;
+ }
+ }
+}
src/main/java/com/keenwrite/processors/DefinitionProcessor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+import java.util.Map;
+
+import static com.keenwrite.processors.text.TextReplacementFactory.replace;
+
+/**
+ * Processes interpolated string definitions in the document and inserts
+ * their values into the post-processed text. The default variable syntax is
+ * {@code $variable$}.
+ */
+public class DefinitionProcessor extends AbstractProcessor<String> {
+
+ private final Map<String, String> mDefinitions;
+
+ public DefinitionProcessor(
+ final Processor<String> successor, final Map<String, String> map ) {
+ super( successor );
+ mDefinitions = map;
+ }
+
+ /**
+ * Processes the given text document by replacing variables with their values.
+ *
+ * @param text The document text that includes variables that should be
+ * replaced with values when rendered as HTML.
+ * @return The text with all variables replaced.
+ */
+ @Override
+ public String apply( final String text ) {
+ return replace( text, getDefinitions() );
+ }
+
+ /**
+ * Returns the map to use for variable substitution.
+ *
+ * @return A map of variable names to values.
+ */
+ protected Map<String, String> getDefinitions() {
+ return mDefinitions;
+ }
+}
src/main/java/com/keenwrite/processors/HtmlPreviewProcessor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+import com.keenwrite.preview.HTMLPreviewPane;
+
+/**
+ * Responsible for notifying the HTMLPreviewPane when the succession chain has
+ * updated. This decouples knowledge of changes to the editor panel from the
+ * HTML preview panel as well as any processing that takes place before the
+ * final HTML preview is rendered. This should be the last link in the processor
+ * chain.
+ */
+public class HtmlPreviewProcessor extends AbstractProcessor<String> {
+
+ // There is only one preview panel.
+ private static HTMLPreviewPane sHtmlPreviewPane;
+
+ /**
+ * Constructs the end of a processing chain.
+ *
+ * @param htmlPreviewPane The pane to update with the post-processed document.
+ */
+ public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
+ sHtmlPreviewPane = htmlPreviewPane;
+ }
+
+ /**
+ * Update the preview panel using HTML from the succession chain.
+ *
+ * @param html The document content to render in the preview pane. The HTML
+ * should not contain a doctype, head, or body tag, only
+ * content to render within the body.
+ * @return {@code null} to indicate no more processors in the chain.
+ */
+ @Override
+ public String apply( final String html ) {
+ getHtmlPreviewPane().process( html );
+
+ // No more processing required.
+ return null;
+ }
+
+ private HTMLPreviewPane getHtmlPreviewPane() {
+ return sHtmlPreviewPane;
+ }
+}
src/main/java/com/keenwrite/processors/IdentityProcessor.java
+/*
+ * Copyright 2017 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+/**
+ * This is the default processor used when an unknown filename extension is
+ * encountered.
+ */
+public class IdentityProcessor extends AbstractProcessor<String> {
+
+ /**
+ * Passes the link to the super constructor.
+ *
+ * @param successor The next processor in the chain to use for text
+ * processing.
+ */
+ public IdentityProcessor( final Processor<String> successor ) {
+ super( successor );
+ }
+
+ /**
+ * Returns the given string, modified with "pre" tags.
+ *
+ * @param t The string to return, enclosed in "pre" tags.
+ * @return The value of t wrapped in "pre" tags.
+ */
+ @Override
+ public String apply( final String t ) {
+ return "<pre>" + t + "</pre>";
+ }
+}
src/main/java/com/keenwrite/processors/InlineRProcessor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+import com.keenwrite.preferences.UserPreferences;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.StringProperty;
+
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import java.io.File;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static com.keenwrite.Constants.STATUS_PARSE_ERROR;
+import static com.keenwrite.StatusBarNotifier.alert;
+import static com.keenwrite.processors.text.TextReplacementFactory.replace;
+import static com.keenwrite.sigils.RSigilOperator.PREFIX;
+import static com.keenwrite.sigils.RSigilOperator.SUFFIX;
+import static java.lang.Math.min;
+
+/**
+ * Transforms a document containing R statements into Markdown.
+ */
+public final class InlineRProcessor extends DefinitionProcessor {
+ /**
+ * Constrain memory when typing new R expressions into the document.
+ */
+ private static final int MAX_CACHED_R_STATEMENTS = 512;
+
+ /**
+ * Where to put document inline evaluated R expressions.
+ */
+ private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
+ @Override
+ protected boolean removeEldestEntry(
+ final Map.Entry<String, Object> eldest ) {
+ return size() > MAX_CACHED_R_STATEMENTS;
+ }
+ };
+
+ /**
+ * Only one editor is open at a time.
+ */
+ private static final ScriptEngine ENGINE =
+ (new ScriptEngineManager()).getEngineByName( "Renjin" );
+
+ private static final int PREFIX_LENGTH = PREFIX.length();
+
+ private final AtomicBoolean mDirty = new AtomicBoolean( false );
+
+ /**
+ * Constructs a processor capable of evaluating R statements.
+ *
+ * @param successor Subsequent link in the processing chain.
+ * @param map Resolved definitions map.
+ */
+ public InlineRProcessor(
+ final Processor<String> successor,
+ final Map<String, String> map ) {
+ super( successor, map );
+
+ bootstrapScriptProperty().addListener(
+ ( ob, oldScript, newScript ) -> setDirty( true ) );
+ workingDirectoryProperty().addListener(
+ ( ob, oldScript, newScript ) -> setDirty( true ) );
+
+ getUserPreferences().addSaveEventHandler( ( handler ) -> {
+ if( isDirty() ) {
+ init();
+ setDirty( false );
+ }
+ } );
+
+ init();
+ }
+
+ /**
+ * Initialises the R code so that R can find imported libraries. Note that
+ * any existing R functionality will not be overwritten if this method is
+ * called multiple times.
+ */
+ private void init() {
+ final var bootstrap = getBootstrapScript();
+
+ if( !bootstrap.isBlank() ) {
+ final var wd = getWorkingDirectory();
+ final var dir = wd.toString().replace( '\\', '/' );
+ final var map = getDefinitions();
+ map.put( "$application.r.working.directory$", dir );
+
+ eval( replace( bootstrap, map ) );
+ }
+ }
+
+ /**
+ * Sets the dirty flag to indicate that the bootstrap script or working
+ * directory has been modified. Upon saving the preferences, if this flag
+ * is true, then {@link #init()} will be called to reload the R environment.
+ *
+ * @param dirty Set to true to reload changes upon closing preferences.
+ */
+ private void setDirty( final boolean dirty ) {
+ mDirty.set( dirty );
+ }
+
+ /**
+ * Answers whether R-related settings have been modified.
+ *
+ * @return {@code true} when the settings have changed.
+ */
+ private boolean isDirty() {
+ return mDirty.get();
+ }
+
+ /**
+ * Evaluates all R statements in the source document and inserts the
+ * calculated value into the generated document.
+ *
+ * @param text The document text that includes variables that should be
+ * replaced with values when rendered as HTML.
+ * @return The generated document with output from all R statements
+ * substituted with value returned from their execution.
+ */
+ @Override
+ public String apply( final String text ) {
+ final int length = text.length();
+
+ // The * 2 is a wild guess at the ratio of R statements to the length
+ // of text produced by those statements.
+ final StringBuilder sb = new StringBuilder( length * 2 );
+
+ int prevIndex = 0;
+ int currIndex = text.indexOf( PREFIX );
+
+ while( currIndex >= 0 ) {
+ // Copy everything up to, but not including, an R statement (`r#).
+ sb.append( text, prevIndex, currIndex );
+
+ // Jump to the start of the R statement.
+ prevIndex = currIndex + PREFIX_LENGTH;
+
+ // Find the statement ending (`), without indexing past the text boundary.
+ currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
+
+ // Only evaluate inline R statements that have end delimiters.
+ if( currIndex > 1 ) {
+ // Extract the inline R statement to be evaluated.
+ final String r = text.substring( prevIndex, currIndex );
+
+ // Pass the R statement into the R engine for evaluation.
+ try {
+ final Object result = evalText( r );
+
+ // Append the string representation of the result into the text.
+ sb.append( result );
+ } catch( final Exception e ) {
+ // If the string couldn't be parsed using R, append the statement
+ // that failed to parse, instead of its evaluated value.
+ sb.append( PREFIX ).append( r ).append( SUFFIX );
+
+ // Tell the user that there was a problem.
+ alert( STATUS_PARSE_ERROR, e.getMessage(), currIndex );
+ }
+
+ // Retain the R statement's ending position in the text.
+ prevIndex = currIndex + 1;
+ }
+
+ // Find the start of the next inline R statement.
+ currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
+ }
+
+ // Copy from the previous index to the end of the string.
+ return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
+ }
+
+ /**
+ * Look up an R expression from the cache then return the resulting object.
+ * If the R expression hasn't been cached, it'll first be evaluated.
+ *
+ * @param r The expression to evaluate.
+ * @return The object resulting from the evaluation.
+ */
+ private Object evalText( final String r ) {
+ return mEvalCache.computeIfAbsent( r, v -> eval( r ) );
+ }
+
+ /**
+ * Evaluate an R expression and return the resulting object.
+ *
+ * @param r The expression to evaluate.
+ * @return The object resulting from the evaluation.
+ */
+ private Object eval( final String r ) {
+ try {
+ return getScriptEngine().eval( r );
+ } catch( final Exception ex ) {
+ final String expr = r.substring( 0, min( r.length(), 30 ) );
+ alert( "Main.status.error.r", expr, ex.getMessage() );
+ }
+
+ return "";
+ }
+
+ /**
+ * Return the given path if not {@code null}, otherwise return the path to
+ * the user's directory.
+ *
+ * @return A non-null path.
+ */
+ private Path getWorkingDirectory() {
+ return getUserPreferences().getRDirectory().toPath();
+ }
+
+ private ObjectProperty<File> workingDirectoryProperty() {
+ return getUserPreferences().rDirectoryProperty();
+ }
+
+ /**
+ * Loads the R init script from the application's persisted preferences.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ private String getBootstrapScript() {
+ return getUserPreferences().getRScript();
+ }
+
+ private StringProperty bootstrapScriptProperty() {
+ return getUserPreferences().rScriptProperty();
+ }
+
+ private UserPreferences getUserPreferences() {
+ return UserPreferences.getInstance();
+ }
+
+ private ScriptEngine getScriptEngine() {
+ return ENGINE;
+ }
+}
src/main/java/com/keenwrite/processors/Processor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+import java.util.function.UnaryOperator;
+
+/**
+ * Responsible for processing documents from one known format to another.
+ * Processes the given content providing a transformation from one document
+ * format into another. For example, this could convert from XML to text using
+ * an XSLT processor, or from markdown to HTML.
+ *
+ * @param <T> The type of processor to create.
+ */
+public interface Processor<T> extends UnaryOperator<T> {
+
+ /**
+ * Removes the given processor from the chain, returning a new immutable
+ * chain equivalent to this chain, but without the given processor.
+ *
+ * @param processor The {@link Processor} to remove from the chain.
+ * @return A delegating processor chain starting from this processor
+ * onwards with the given processor removed from the chain.
+ */
+ Processor<T> remove( Class<? extends Processor<T>> processor );
+
+ /**
+ * Adds a document processor to call after this processor finishes processing
+ * the document given to the process method.
+ *
+ * @return The processor that should transform the document after this
+ * instance has finished processing, or {@code null} if this is the last
+ * processor in the chain.
+ */
+ default Processor<T> next() {
+ return null;
+ }
+}
src/main/java/com/keenwrite/processors/ProcessorFactory.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+import com.keenwrite.AbstractFileFactory;
+import com.keenwrite.FileEditorTab;
+import com.keenwrite.preview.HTMLPreviewPane;
+import com.keenwrite.processors.markdown.MarkdownProcessor;
+
+import java.util.Map;
+
+/**
+ * Responsible for creating processors capable of parsing, transforming,
+ * interpolating, and rendering known file types.
+ */
+public class ProcessorFactory extends AbstractFileFactory {
+
+ private final HTMLPreviewPane mPreviewPane;
+ private final Map<String, String> mResolvedMap;
+ private final Processor<String> mMarkdownProcessor;
+
+ /**
+ * Constructs a factory with the ability to create processors that can perform
+ * text and caret processing to generate a final preview.
+ *
+ * @param previewPane Where the final output is rendered.
+ * @param resolvedMap Flat map of definitions to replace before final render.
+ */
+ public ProcessorFactory(
+ final HTMLPreviewPane previewPane,
+ final Map<String, String> resolvedMap ) {
+ mPreviewPane = previewPane;
+ mResolvedMap = resolvedMap;
+ mMarkdownProcessor = createMarkdownProcessor();
+ }
+
+ /**
+ * Creates a processor chain suitable for parsing and rendering the file
+ * opened at the given tab.
+ *
+ * @param tab The tab containing a text editor, path, and caret position.
+ * @return A processor that can render the given tab's text.
+ */
+ public Processor<String> createProcessors( final FileEditorTab tab ) {
+ return switch( lookup( tab.getPath() ) ) {
+ case RMARKDOWN -> createRProcessor();
+ case SOURCE -> createMarkdownDefinitionProcessor();
+ case XML -> createXMLProcessor( tab );
+ case RXML -> createRXMLProcessor( tab );
+ default -> createIdentityProcessor();
+ };
+ }
+
+ private Processor<String> createHTMLPreviewProcessor() {
+ return new HtmlPreviewProcessor( getPreviewPane() );
+ }
+
+ /**
+ * Creates and links the processors at the end of the processing chain.
+ *
+ * @return A markdown, caret replacement, and preview pane processor chain.
+ */
+ private Processor<String> createMarkdownProcessor() {
+ final var hpp = createHTMLPreviewProcessor();
+ return new MarkdownProcessor( hpp, getPreviewPane().getPath() );
+ }
+
+ protected Processor<String> createIdentityProcessor() {
+ final var hpp = createHTMLPreviewProcessor();
+ return new IdentityProcessor( hpp );
+ }
+
+ protected Processor<String> createDefinitionProcessor(
+ final Processor<String> p ) {
+ return new DefinitionProcessor( p, getResolvedMap() );
+ }
+
+ protected Processor<String> createMarkdownDefinitionProcessor() {
+ final var tpc = getCommonProcessor();
+ return createDefinitionProcessor( tpc );
+ }
+
+ protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
+ final var tpc = getCommonProcessor();
+ final var xmlp = new XmlProcessor( tpc, tab.getPath() );
+ return createDefinitionProcessor( xmlp );
+ }
+
+ protected Processor<String> createRProcessor() {
+ final var tpc = getCommonProcessor();
+ final var rp = new InlineRProcessor( tpc, getResolvedMap() );
+ return new RVariableProcessor( rp, getResolvedMap() );
+ }
+
+ protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
+ final var tpc = getCommonProcessor();
+ final var xmlp = new XmlProcessor( tpc, tab.getPath() );
+ final var rp = new InlineRProcessor( xmlp, getResolvedMap() );
+ return new RVariableProcessor( rp, getResolvedMap() );
+ }
+
+ private HTMLPreviewPane getPreviewPane() {
+ return mPreviewPane;
+ }
+
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
+ private Map<String, String> getResolvedMap() {
+ return mResolvedMap;
+ }
+
+ /**
+ * Returns a processor common to all processors: markdown, caret position
+ * token replacer, and an HTML preview renderer.
+ *
+ * @return Processors at the end of the processing chain.
+ */
+ private Processor<String> getCommonProcessor() {
+ return mMarkdownProcessor;
+ }
+}
src/main/java/com/keenwrite/processors/RVariableProcessor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+import com.keenwrite.sigils.RSigilOperator;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Converts the keys of the resolved map from default form to R form, then
+ * performs a substitution on the text. The default R variable syntax is
+ * {@code v$tree$leaf}.
+ */
+public class RVariableProcessor extends DefinitionProcessor {
+
+ public RVariableProcessor(
+ final Processor<String> rp, final Map<String, String> map ) {
+ super( rp, map );
+ }
+
+ /**
+ * Returns the R-based version of the interpolated variable definitions.
+ *
+ * @return Variable names transmogrified from the default syntax to R syntax.
+ */
+ @Override
+ protected Map<String, String> getDefinitions() {
+ return toR( super.getDefinitions() );
+ }
+
+ /**
+ * Converts the given map from regular variables to R variables.
+ *
+ * @param map Map of variable names to values.
+ * @return Map of R variables.
+ */
+ private Map<String, String> toR( final Map<String, String> map ) {
+ final var rMap = new HashMap<String, String>( map.size() );
+
+ for( final var entry : map.entrySet() ) {
+ final var key = entry.getKey();
+ rMap.put( RSigilOperator.entoken( key ), toRValue( map.get( key ) ) );
+ }
+
+ return rMap;
+ }
+
+ private String toRValue( final String value ) {
+ return '\'' + escape( value, '\'', "\\'" ) + '\'';
+ }
+
+ /**
+ * TODO: Make generic method for replacing text.
+ *
+ * @param haystack Search this string for the needle, must not be null.
+ * @param needle The character to find in the haystack.
+ * @param thread Replace the needle with this text, if the needle is found.
+ * @return The haystack with the all instances of needle replaced with thread.
+ */
+ @SuppressWarnings("SameParameterValue")
+ private String escape(
+ final String haystack, final char needle, final String thread ) {
+ int end = haystack.indexOf( needle );
+
+ if( end < 0 ) {
+ return haystack;
+ }
+
+ final int length = haystack.length();
+ int start = 0;
+
+ // Replace up to 32 occurrences before the string reallocates its buffer.
+ final StringBuilder sb = new StringBuilder( length + 32 );
+
+ while( end >= 0 ) {
+ sb.append( haystack, start, end ).append( thread );
+ start = end + 1;
+ end = haystack.indexOf( needle, start );
+ }
+
+ return sb.append( haystack.substring( start ) ).toString();
+ }
+}
src/main/java/com/keenwrite/processors/XmlProcessor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors;
+
+import com.keenwrite.Services;
+import com.keenwrite.service.Snitch;
+import net.sf.saxon.TransformerFactoryImpl;
+import net.sf.saxon.trans.XPathException;
+
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.events.ProcessingInstruction;
+import javax.xml.stream.events.XMLEvent;
+import javax.xml.transform.*;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+import java.io.File;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
+
+/**
+ * Transforms an XML document. The XML document must have a stylesheet specified
+ * as part of its processing instructions, such as:
+ * <p>
+ * {@code xml-stylesheet type="text/xsl" href="markdown.xsl"}
+ * </p>
+ * <p>
+ * The XSL must transform the XML document into Markdown, or another format
+ * recognized by the next link on the chain.
+ * </p>
+ */
+public class XmlProcessor extends AbstractProcessor<String>
+ implements ErrorListener {
+
+ private final Snitch snitch = Services.load( Snitch.class );
+
+ private XMLInputFactory xmlInputFactory;
+ private TransformerFactory transformerFactory;
+ private Transformer transformer;
+
+ private Path path;
+
+ /**
+ * Constructs an XML processor that can transform an XML document into another
+ * format based on the XSL file specified as a processing instruction. The
+ * path must point to the directory where the XSL file is found, which implies
+ * that they must be in the same directory.
+ *
+ * @param processor Next link in the processing chain.
+ * @param path The path to the XML file content to be processed.
+ */
+ public XmlProcessor( final Processor<String> processor, final Path path ) {
+ super( processor );
+ setPath( path );
+ }
+
+ /**
+ * Transforms the given XML text into another form (typically Markdown).
+ *
+ * @param text The text to transform, can be empty, cannot be null.
+ * @return The transformed text, or empty if text is empty.
+ */
+ @Override
+ public String apply( final String text ) {
+ try {
+ return text.isEmpty() ? text : transform( text );
+ } catch( final Exception ex ) {
+ throw new RuntimeException( ex );
+ }
+ }
+
+ /**
+ * Performs an XSL transformation on the given XML text. The XML text must
+ * have a processing instruction that points to the XSL template file to use
+ * for the transformation.
+ *
+ * @param text The text to transform.
+ * @return The transformed text.
+ */
+ private String transform( final String text ) throws Exception {
+ // Extract the XML stylesheet processing instruction.
+ final String template = getXsltFilename( text );
+ final Path xsl = getXslPath( template );
+
+ try(
+ final StringWriter output = new StringWriter( text.length() );
+ final StringReader input = new StringReader( text ) ) {
+
+ // Listen for external file modification events.
+ getSnitch().listen( xsl );
+
+ getTransformer( xsl ).transform(
+ new StreamSource( input ),
+ new StreamResult( output )
+ );
+
+ return output.toString();
+ }
+ }
+
+ /**
+ * Returns an XSL transformer ready to transform an XML document using the
+ * XSLT file specified by the given path. If the path is already known then
+ * this will return the associated transformer.
+ *
+ * @param xsl The path to an XSLT file.
+ * @return A transformer that will transform XML documents using the given
+ * XSLT file.
+ * @throws TransformerConfigurationException Could not instantiate the
+ * transformer.
+ */
+ private Transformer getTransformer( final Path xsl )
+ throws TransformerConfigurationException {
+ if( this.transformer == null ) {
+ this.transformer = createTransformer( xsl );
+ }
+
+ return this.transformer;
+ }
+
+ /**
+ * Creates a configured transformer ready to run.
+ *
+ * @param xsl The stylesheet to use for transforming XML documents.
+ * @return The edited XML document transformed into another format (usually
+ * markdown).
+ * @throws TransformerConfigurationException Could not create the transformer.
+ */
+ protected Transformer createTransformer( final Path xsl )
+ throws TransformerConfigurationException {
+ final Source xslt = new StreamSource( xsl.toFile() );
+
+ return getTransformerFactory().newTransformer( xslt );
+ }
+
+ private Path getXslPath( final String filename ) {
+ final Path xmlPath = getPath();
+ final File xmlDirectory = xmlPath.toFile().getParentFile();
+
+ return Paths.get( xmlDirectory.getPath(), filename );
+ }
+
+ /**
+ * Given XML text, this will use a StAX pull reader to obtain the XML
+ * stylesheet processing instruction. This will throw a parse exception if the
+ * href pseudo-attribute filename value cannot be found.
+ *
+ * @param xml The XML containing an xml-stylesheet processing instruction.
+ * @return The href pseudo-attribute value.
+ * @throws XMLStreamException Could not parse the XML file.
+ */
+ private String getXsltFilename( final String xml )
+ throws XMLStreamException, XPathException {
+
+ String result = "";
+
+ try( final StringReader sr = new StringReader( xml ) ) {
+ boolean found = false;
+ int count = 0;
+ final XMLEventReader reader = createXMLEventReader( sr );
+
+ // If the processing instruction wasn't found in the first 10 lines,
+ // fail fast. This should iterate twice through the loop.
+ while( !found && reader.hasNext() && count++ < 10 ) {
+ final XMLEvent event = reader.nextEvent();
+
+ if( event.isProcessingInstruction() ) {
+ final ProcessingInstruction pi = (ProcessingInstruction) event;
+ final String target = pi.getTarget();
+
+ if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
+ result = getPseudoAttribute( pi.getData(), "href" );
+ found = true;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ private XMLEventReader createXMLEventReader( final Reader reader )
+ throws XMLStreamException {
+ return getXMLInputFactory().createXMLEventReader( reader );
+ }
+
+ private synchronized XMLInputFactory getXMLInputFactory() {
+ if( this.xmlInputFactory == null ) {
+ this.xmlInputFactory = createXMLInputFactory();
+ }
+
+ return this.xmlInputFactory;
+ }
+
+ private XMLInputFactory createXMLInputFactory() {
+ return XMLInputFactory.newInstance();
+ }
+
+ private synchronized TransformerFactory getTransformerFactory() {
+ if( this.transformerFactory == null ) {
+ this.transformerFactory = createTransformerFactory();
+ }
+
+ return this.transformerFactory;
+ }
+
+ /**
+ * Returns a high-performance XSLT 2 transformation engine.
+ *
+ * @return An XSL transforming engine.
+ */
+ private TransformerFactory createTransformerFactory() {
+ final TransformerFactory factory = new TransformerFactoryImpl();
+
+ // Bubble problems up to the user interface, rather than standard error.
+ factory.setErrorListener( this );
+
+ return factory;
+ }
+
+ /**
+ * Called when the XSL transformer issues a warning.
+ *
+ * @param ex The problem the transformer encountered.
+ */
+ @Override
+ public void warning( final TransformerException ex ) {
+ throw new RuntimeException( ex );
+ }
+
+ /**
+ * Called when the XSL transformer issues an error.
+ *
+ * @param ex The problem the transformer encountered.
+ */
+ @Override
+ public void error( final TransformerException ex ) {
+ throw new RuntimeException( ex );
+ }
+
+ /**
+ * Called when the XSL transformer issues a fatal error, which is probably
+ * a bit over-dramatic a method name.
+ *
+ * @param ex The problem the transformer encountered.
+ */
+ @Override
+ public void fatalError( final TransformerException ex ) {
+ throw new RuntimeException( ex );
+ }
+
+ private void setPath( final Path path ) {
+ this.path = path;
+ }
+
+ private Path getPath() {
+ return this.path;
+ }
+
+ private Snitch getSnitch() {
+ return this.snitch;
+ }
+}
src/main/java/com/keenwrite/processors/markdown/BlockExtension.java
+package com.keenwrite.processors.markdown;
+
+import com.vladsch.flexmark.ast.BlockQuote;
+import com.vladsch.flexmark.ast.ListBlock;
+import com.vladsch.flexmark.html.AttributeProvider;
+import com.vladsch.flexmark.html.AttributeProviderFactory;
+import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
+import com.vladsch.flexmark.html.renderer.AttributablePart;
+import com.vladsch.flexmark.html.renderer.LinkResolverContext;
+import com.vladsch.flexmark.util.ast.Block;
+import com.vladsch.flexmark.util.ast.Node;
+import com.vladsch.flexmark.util.data.MutableDataHolder;
+import com.vladsch.flexmark.util.html.MutableAttributes;
+import org.jetbrains.annotations.NotNull;
+
+import static com.keenwrite.Constants.PARAGRAPH_ID_PREFIX;
+import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
+import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
+import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
+
+/**
+ * Responsible for giving most block-level elements a unique identifier
+ * attribute. The identifier is used to coordinate scrolling.
+ */
+public class BlockExtension implements HtmlRendererExtension {
+ /**
+ * Responsible for creating the id attribute. This class is instantiated
+ * each time the document is rendered, thereby resetting the count to zero.
+ */
+ public static class IdAttributeProvider implements AttributeProvider {
+ private int mCount;
+
+ private static AttributeProviderFactory createFactory() {
+ return new IndependentAttributeProviderFactory() {
+ @Override
+ public @NotNull AttributeProvider apply(
+ @NotNull final LinkResolverContext context ) {
+ return new IdAttributeProvider();
+ }
+ };
+ }
+
+ @Override
+ public void setAttributes( @NotNull Node node,
+ @NotNull AttributablePart part,
+ @NotNull MutableAttributes attributes ) {
+ // Blockquotes are troublesome because they can interleave blank lines
+ // without having an equivalent blank line in the source document. That
+ // is, in Markdown the > symbol on a line by itself will generate a blank
+ // line in the resulting document; however, a > symbol in the text editor
+ // does not count as a blank line. Resolving this issue is tricky.
+ //
+ // The CODE_CONTENT represents <code> embedded inside <pre>; both elements
+ // enter this method as FencedCodeBlock, but only the <pre> must be
+ // uniquely identified (because they are the same line in Markdown).
+ //
+ if( node instanceof Block &&
+ !(node instanceof BlockQuote) &&
+ !(node instanceof ListBlock) &&
+ (part != CODE_CONTENT) ) {
+ attributes.addValue( "id", PARAGRAPH_ID_PREFIX + mCount++ );
+ }
+ }
+ }
+
+ private BlockExtension() {
+ }
+
+ @Override
+ public void extend( final Builder builder,
+ @NotNull final String rendererType ) {
+ builder.attributeProviderFactory( IdAttributeProvider.createFactory() );
+ }
+
+ public static BlockExtension create() {
+ return new BlockExtension();
+ }
+
+ @Override
+ public void rendererOptions( @NotNull final MutableDataHolder options ) {
+ }
+}
src/main/java/com/keenwrite/processors/markdown/ImageLinkExtension.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.markdown;
+
+import com.keenwrite.preferences.UserPreferences;
+import com.vladsch.flexmark.ast.Image;
+import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
+import com.vladsch.flexmark.html.LinkResolver;
+import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
+import com.vladsch.flexmark.html.renderer.LinkStatus;
+import com.vladsch.flexmark.html.renderer.ResolvedLink;
+import com.vladsch.flexmark.util.ast.Node;
+import com.vladsch.flexmark.util.data.MutableDataHolder;
+import org.jetbrains.annotations.NotNull;
+import org.renjin.repackaged.guava.base.Splitter;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.nio.file.Path;
+
+import static com.keenwrite.StatusBarNotifier.alert;
+import static com.keenwrite.util.ProtocolResolver.getProtocol;
+import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
+import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
+import static java.lang.String.format;
+
+/**
+ * Responsible for ensuring that images can be rendered relative to a path.
+ * This allows images to be located virtually anywhere.
+ */
+public class ImageLinkExtension implements HtmlRendererExtension {
+
+ /**
+ * Creates an extension capable of using a relative path to embed images.
+ *
+ * @param path The {@link Path} to the file being edited; the parent path
+ * is the starting location of the relative image directory.
+ * @return The new {@link ImageLinkExtension}, never {@code null}.
+ */
+ public static ImageLinkExtension create( @NotNull final Path path ) {
+ return new ImageLinkExtension( path );
+ }
+
+ private class Factory extends IndependentLinkResolverFactory {
+ @Override
+ public @NotNull LinkResolver apply(
+ @NotNull final LinkResolverBasicContext context ) {
+ return new ImageLinkResolver();
+ }
+ }
+
+ private class ImageLinkResolver implements LinkResolver {
+ private final UserPreferences mUserPref = getUserPreferences();
+ private final File mImagesUserPrefix = mUserPref.getImagesDirectory();
+ private final String mImageExtensions = mUserPref.getImagesOrder();
+
+ public ImageLinkResolver() {
+ }
+
+ /**
+ * You can also set/clear/modify attributes through
+ * {@link ResolvedLink#getAttributes()} and
+ * {@link ResolvedLink#getNonNullAttributes()}.
+ */
+ @NotNull
+ @Override
+ public ResolvedLink resolveLink(
+ @NotNull final Node node,
+ @NotNull final LinkResolverBasicContext context,
+ @NotNull final ResolvedLink link ) {
+ return node instanceof Image ? resolve( link ) : link;
+ }
+
+ private ResolvedLink resolve( final ResolvedLink link ) {
+ var url = link.getUrl();
+ final var protocol = getProtocol( url );
+
+ try {
+ // If the direct file name exists, then use it directly.
+ if( (protocol.isFile() && Path.of( url ).toFile().exists()) ||
+ protocol.isHttp() ) {
+ return valid( link, url );
+ }
+ } catch( final Exception ignored ) {
+ // Try to resolve the image, dynamically.
+ }
+
+ try {
+ final Path imagePrefix = getImagePrefix().toPath();
+
+ // Path to the file being edited.
+ Path editPath = getEditPath();
+
+ // If there is no parent path to the file, it means the file has not
+ // been saved. Default to using the value from the user's preferences.
+ // The user's preferences will be defaulted to a the application's
+ // starting directory.
+ if( editPath == null ) {
+ editPath = imagePrefix;
+ }
+ else {
+ editPath = Path.of( editPath.toString(), imagePrefix.toString() );
+ }
+
+ final Path imagePathPrefix = Path.of( editPath.toString(), url );
+ final String suffixes = getImageExtensions();
+ boolean missing = true;
+
+ // Iterate over the user's preferred image file type extensions.
+ for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) {
+ final String imagePath = format( "%s.%s", imagePathPrefix, ext );
+ final File file = new File( imagePath );
+
+ if( file.exists() ) {
+ url = file.toString();
+ missing = false;
+ break;
+ }
+ }
+
+ if( missing ) {
+ throw new FileNotFoundException( imagePathPrefix + ".*" );
+ }
+
+ if( protocol.isFile() ) {
+ url = "file://" + url;
+ }
+
+ return valid( link, url );
+ } catch( final Exception ex ) {
+ alert( ex );
+ }
+
+ return link;
+ }
+
+ private ResolvedLink valid( final ResolvedLink link, final String url ) {
+ return link.withStatus( LinkStatus.VALID ).withUrl( url );
+ }
+
+ private File getImagePrefix() {
+ return mImagesUserPrefix;
+ }
+
+ private String getImageExtensions() {
+ return mImageExtensions;
+ }
+
+ private Path getEditPath() {
+ return mPath.getParent();
+ }
+ }
+
+ private final Path mPath;
+
+ private ImageLinkExtension( @NotNull final Path path ) {
+ mPath = path;
+ }
+
+ @Override
+ public void rendererOptions( @NotNull final MutableDataHolder options ) {
+ }
+
+ @Override
+ public void extend( @NotNull final Builder builder,
+ @NotNull final String rendererType ) {
+ builder.linkResolverFactory( new Factory() );
+ }
+
+ private UserPreferences getUserPreferences() {
+ return UserPreferences.getInstance();
+ }
+}
src/main/java/com/keenwrite/processors/markdown/LigatureExtension.java
+package com.keenwrite.processors.markdown;
+
+import com.vladsch.flexmark.ast.Text;
+import com.vladsch.flexmark.html.HtmlWriter;
+import com.vladsch.flexmark.html.renderer.NodeRenderer;
+import com.vladsch.flexmark.html.renderer.NodeRendererContext;
+import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
+import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
+import com.vladsch.flexmark.util.ast.TextCollectingVisitor;
+import com.vladsch.flexmark.util.data.DataHolder;
+import com.vladsch.flexmark.util.data.MutableDataHolder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static com.keenwrite.processors.text.TextReplacementFactory.replace;
+import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
+import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
+
+/**
+ * Responsible for substituting multi-codepoint glyphs with single codepoint
+ * glyphs. The text is adorned with ligatures prior to rendering as HTML.
+ * This requires a font that supports ligatures.
+ * <p>
+ * TODO: #81 -- I18N
+ * </p>
+ */
+public class LigatureExtension implements HtmlRendererExtension {
+ /**
+ * Retain insertion order so that ligature substitution uses longer ligatures
+ * ahead of shorter ligatures. The word "ruffian" should use the "ffi"
+ * ligature, not the "ff" ligature.
+ */
+ private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
+
+ static {
+ LIGATURES.put( "ffi", "\uFB03" );
+ LIGATURES.put( "ffl", "\uFB04" );
+ LIGATURES.put( "ff", "\uFB00" );
+ LIGATURES.put( "fi", "\uFB01" );
+ LIGATURES.put( "fl", "\uFB02" );
+ LIGATURES.put( "ft", "\uFB05" );
+ LIGATURES.put( "AE", "\u00C6" );
+ LIGATURES.put( "OE", "\u0152" );
+// "ae", "\u00E6",
+// "oe", "\u0153",
+ }
+
+ private static class LigatureRenderer implements NodeRenderer {
+ private final TextCollectingVisitor mVisitor = new TextCollectingVisitor();
+
+ @SuppressWarnings("unused")
+ public LigatureRenderer( final DataHolder options ) {
+ }
+
+ @Override
+ public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
+ return Set.of( new NodeRenderingHandler<>(
+ Text.class, LigatureRenderer.this::render ) );
+ }
+
+ /**
+ * This will pick the fastest string replacement algorithm based on the
+ * text length. The insertion order of the {@link #LIGATURES} is
+ * important to give precedence to longer ligatures.
+ *
+ * @param textNode The text node containing text to replace with ligatures.
+ * @param context Not used.
+ * @param html Where to write the text adorned with ligatures.
+ */
+ private void render(
+ @NotNull final Text textNode,
+ @NotNull final NodeRendererContext context,
+ @NotNull final HtmlWriter html ) {
+ final var text = mVisitor.collectAndGetText( textNode );
+ html.text( replace( text, LIGATURES ) );
+ }
+ }
+
+ private static class Factory implements NodeRendererFactory {
+ @NotNull
+ @Override
+ public NodeRenderer apply( @NotNull DataHolder options ) {
+ return new LigatureRenderer( options );
+ }
+ }
+
+ private LigatureExtension() {
+ }
+
+ @Override
+ public void rendererOptions( @NotNull final MutableDataHolder options ) {
+ }
+
+ @Override
+ public void extend( @NotNull final Builder builder,
+ @NotNull final String rendererType ) {
+ if( "HTML".equalsIgnoreCase( rendererType ) ) {
+ builder.nodeRendererFactory( new Factory() );
+ }
+ }
+
+ public static LigatureExtension create() {
+ return new LigatureExtension();
+ }
+}
src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.markdown;
+
+import com.keenwrite.processors.AbstractProcessor;
+import com.keenwrite.processors.Processor;
+import com.vladsch.flexmark.ext.definition.DefinitionExtension;
+import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
+import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
+import com.vladsch.flexmark.ext.tables.TablesExtension;
+import com.vladsch.flexmark.ext.typographic.TypographicExtension;
+import com.vladsch.flexmark.html.HtmlRenderer;
+import com.vladsch.flexmark.parser.Parser;
+import com.vladsch.flexmark.util.ast.IParse;
+import com.vladsch.flexmark.util.ast.Node;
+import com.vladsch.flexmark.util.misc.Extension;
+
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import static com.keenwrite.Constants.USER_DIRECTORY;
+
+/**
+ * Responsible for parsing a Markdown document and rendering it as HTML.
+ */
+public class MarkdownProcessor extends AbstractProcessor<String> {
+
+ private final HtmlRenderer mRenderer;
+ private final IParse mParser;
+
+ public MarkdownProcessor(
+ final Processor<String> successor ) {
+ this( successor, Path.of( USER_DIRECTORY ) );
+ }
+
+ /**
+ * Constructs a new Markdown processor that can create HTML documents.
+ *
+ * @param successor Usually the HTML Preview Processor.
+ */
+ public MarkdownProcessor(
+ final Processor<String> successor, final Path path ) {
+ super( successor );
+
+ // Standard extensions
+ final Collection<Extension> extensions = new ArrayList<>();
+ extensions.add( DefinitionExtension.create() );
+ extensions.add( StrikethroughSubscriptExtension.create() );
+ extensions.add( SuperscriptExtension.create() );
+ extensions.add( TablesExtension.create() );
+ extensions.add( TypographicExtension.create() );
+
+ // Allows referencing image files via relative paths and dynamic file types.
+ extensions.add( ImageLinkExtension.create( path ) );
+ extensions.add( BlockExtension.create() );
+ extensions.add( TeXExtension.create() );
+
+ // TODO: https://github.com/FAlthausen/Vollkorn-Typeface/issues/38
+ // TODO: Uncomment when Vollkorn ligatures are fixed.
+ // extensions.add( LigatureExtension.create() );
+
+ mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
+ mParser = Parser.builder()
+ .extensions( extensions )
+ .build();
+ }
+
+ /**
+ * Converts the given Markdown string into HTML, without the doctype, html,
+ * head, and body tags.
+ *
+ * @param markdown The string to convert from Markdown to HTML.
+ * @return The HTML representation of the Markdown document.
+ */
+ @Override
+ public String apply( final String markdown ) {
+ return toHtml( markdown );
+ }
+
+ /**
+ * Returns the AST in the form of a node for the given markdown document. This
+ * can be used, for example, to determine if a hyperlink exists inside of a
+ * paragraph.
+ *
+ * @param markdown The markdown to convert into an AST.
+ * @return The markdown AST for the given text (usually a paragraph).
+ */
+ public Node toNode( final String markdown ) {
+ return parse( markdown );
+ }
+
+ /**
+ * Helper method to create an AST given some markdown.
+ *
+ * @param markdown The markdown to parse.
+ * @return The root node of the markdown tree.
+ */
+ private Node parse( final String markdown ) {
+ return getParser().parse( markdown );
+ }
+
+ /**
+ * Converts a string of markdown into HTML.
+ *
+ * @param markdown The markdown text to convert to HTML, must not be null.
+ * @return The markdown rendered as an HTML document.
+ */
+ private String toHtml( final String markdown ) {
+ return getRenderer().render( parse( markdown ) );
+ }
+
+ /**
+ * Creates the Markdown document processor.
+ *
+ * @return A Parser that can build an abstract syntax tree.
+ */
+ private IParse getParser() {
+ return mParser;
+ }
+
+ private HtmlRenderer getRenderer() {
+ return mRenderer;
+ }
+}
src/main/java/com/keenwrite/processors/markdown/TeXExtension.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.markdown;
+
+import com.keenwrite.processors.markdown.tex.TeXInlineDelimiterProcessor;
+import com.keenwrite.processors.markdown.tex.TeXNodeRenderer;
+import com.vladsch.flexmark.html.HtmlRenderer;
+import com.vladsch.flexmark.parser.Parser;
+import com.vladsch.flexmark.util.data.MutableDataHolder;
+import org.jetbrains.annotations.NotNull;
+
+import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
+import static com.vladsch.flexmark.parser.Parser.ParserExtension;
+
+/**
+ * Responsible for wrapping delimited TeX code in Markdown into an XML element
+ * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
+ * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
+ * is responsible for converting the TeX code for display. This avoids inserting
+ * SVG code into the Markdown document, which the parser would then have to
+ * iterate---a <em>very</em> wasteful operation that impacts front-end
+ * performance.
+ */
+public class TeXExtension implements ParserExtension, HtmlRendererExtension {
+ /**
+ * Creates an extension capable of handling delimited TeX code in Markdown.
+ *
+ * @return The new {@link TeXExtension}, never {@code null}.
+ */
+ public static TeXExtension create() {
+ return new TeXExtension();
+ }
+
+ /**
+ * Force using the {@link #create()} method for consistency.
+ */
+ private TeXExtension() {
+ }
+
+ /**
+ * Adds the TeX extension for HTML document export types.
+ *
+ * @param builder The document builder.
+ * @param rendererType Indicates the document type to be built.
+ */
+ @Override
+ public void extend( @NotNull final HtmlRenderer.Builder builder,
+ @NotNull final String rendererType ) {
+ if( "HTML".equalsIgnoreCase( rendererType ) ) {
+ builder.nodeRendererFactory( new TeXNodeRenderer.Factory() );
+ }
+ }
+
+ @Override
+ public void extend( final Parser.Builder builder ) {
+ builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() );
+ }
+
+ @Override
+ public void rendererOptions( @NotNull final MutableDataHolder options ) {
+ }
+
+ @Override
+ public void parserOptions( final MutableDataHolder options ) {
+ }
+}
src/main/java/com/keenwrite/processors/markdown/tex/TeXInlineDelimiterProcessor.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.markdown.tex;
+
+import com.vladsch.flexmark.parser.InlineParser;
+import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
+import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
+import com.vladsch.flexmark.parser.delimiter.DelimiterRun;
+import com.vladsch.flexmark.util.ast.Node;
+
+public class TeXInlineDelimiterProcessor implements DelimiterProcessor {
+
+ @Override
+ public void process( final Delimiter opener, final Delimiter closer,
+ final int delimitersUsed ) {
+ final var node = new TeXNode();
+ opener.moveNodesBetweenDelimitersTo(node, closer);
+ }
+
+ @Override
+ public char getOpeningCharacter() {
+ return '$';
+ }
+
+ @Override
+ public char getClosingCharacter() {
+ return '$';
+ }
+
+ @Override
+ public int getMinLength() {
+ return 1;
+ }
+
+ /**
+ * Allow for $ or $$.
+ *
+ * @param opener One or more opening delimiter characters.
+ * @param closer One or more closing delimiter characters.
+ * @return The number of delimiters to use to determine whether a valid
+ * opening delimiter expression is found.
+ */
+ @Override
+ public int getDelimiterUse(
+ final DelimiterRun opener, final DelimiterRun closer ) {
+ return 1;
+ }
+
+ @Override
+ public boolean canBeOpener( final String before,
+ final String after,
+ final boolean leftFlanking,
+ final boolean rightFlanking,
+ final boolean beforeIsPunctuation,
+ final boolean afterIsPunctuation,
+ final boolean beforeIsWhitespace,
+ final boolean afterIsWhiteSpace ) {
+ return leftFlanking;
+ }
+
+ @Override
+ public boolean canBeCloser( final String before,
+ final String after,
+ final boolean leftFlanking,
+ final boolean rightFlanking,
+ final boolean beforeIsPunctuation,
+ final boolean afterIsPunctuation,
+ final boolean beforeIsWhitespace,
+ final boolean afterIsWhiteSpace ) {
+ return rightFlanking;
+ }
+
+ @Override
+ public Node unmatchedDelimiterNode(
+ final InlineParser inlineParser, final DelimiterRun delimiter ) {
+ return null;
+ }
+
+ @Override
+ public boolean skipNonOpenerCloser() {
+ return false;
+ }
+}
src/main/java/com/keenwrite/processors/markdown/tex/TeXNode.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.markdown.tex;
+
+import com.vladsch.flexmark.ast.DelimitedNodeImpl;
+
+public class TeXNode extends DelimitedNodeImpl {
+ /**
+ * TeX expression wrapped in a {@code <tex>} element.
+ */
+ public static final String HTML_TEX = "tex";
+
+ public TeXNode() {
+ }
+}
src/main/java/com/keenwrite/processors/markdown/tex/TeXNodeRenderer.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.markdown.tex;
+
+import com.vladsch.flexmark.html.HtmlWriter;
+import com.vladsch.flexmark.html.renderer.NodeRenderer;
+import com.vladsch.flexmark.html.renderer.NodeRendererContext;
+import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
+import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
+import com.vladsch.flexmark.util.data.DataHolder;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Set;
+
+import static com.keenwrite.processors.markdown.tex.TeXNode.HTML_TEX;
+
+public class TeXNodeRenderer implements NodeRenderer {
+
+ public static class Factory implements NodeRendererFactory {
+ @NotNull
+ @Override
+ public NodeRenderer apply( @NotNull DataHolder options ) {
+ return new TeXNodeRenderer();
+ }
+ }
+
+ @Override
+ public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
+ return Set.of( new NodeRenderingHandler<>( TeXNode.class, this::render ) );
+ }
+
+ private void render( final TeXNode node,
+ final NodeRendererContext context,
+ final HtmlWriter html ) {
+ html.tag( HTML_TEX );
+ html.raw( node.getText() );
+ html.closeTag( HTML_TEX );
+ }
+}
src/main/java/com/keenwrite/processors/text/AbstractTextReplacer.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.text;
+
+import java.util.Map;
+
+/**
+ * Responsible for common behaviour across all text replacer implementations.
+ */
+public abstract class AbstractTextReplacer implements TextReplacer {
+
+ /**
+ * Default (empty) constructor.
+ */
+ protected AbstractTextReplacer() {
+ }
+
+ protected String[] keys( final Map<String, String> map ) {
+ return map.keySet().toArray( new String[ 0 ] );
+ }
+
+ protected String[] values( final Map<String, String> map ) {
+ return map.values().toArray( new String[ 0 ] );
+ }
+}
src/main/java/com/keenwrite/processors/text/AhoCorasickReplacer.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.text;
+
+import java.util.Map;
+import org.ahocorasick.trie.Emit;
+import org.ahocorasick.trie.Trie.TrieBuilder;
+import static org.ahocorasick.trie.Trie.builder;
+
+/**
+ * Replaces text using an Aho-Corasick algorithm.
+ */
+public class AhoCorasickReplacer extends AbstractTextReplacer {
+
+ /**
+ * Default (empty) constructor.
+ */
+ protected AhoCorasickReplacer() {
+ }
+
+ @Override
+ public String replace( final String text, final Map<String, String> map ) {
+ // Create a buffer sufficiently large that re-allocations are minimized.
+ final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
+
+ // The TrieBuilder should only match whole words and ignore overlaps (there
+ // shouldn't be any).
+ final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps();
+
+ for( final String key : keys( map ) ) {
+ builder.addKeyword( key );
+ }
+
+ int index = 0;
+
+ // Replace all instances with dereferenced variables.
+ for( final Emit emit : builder.build().parseText( text ) ) {
+ sb.append( text, index, emit.getStart() );
+ sb.append( map.get( emit.getKeyword() ) );
+ index = emit.getEnd() + 1;
+ }
+
+ // Add the remainder of the string (contains no more matches).
+ sb.append( text.substring( index ) );
+
+ return sb.toString();
+ }
+}
src/main/java/com/keenwrite/processors/text/StringUtilsReplacer.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.text;
+
+import java.util.Map;
+
+import static org.apache.commons.lang3.StringUtils.replaceEach;
+
+/**
+ * Replaces text using Apache's StringUtils.replaceEach method.
+ */
+public class StringUtilsReplacer extends AbstractTextReplacer {
+
+ /**
+ * Default (empty) constructor.
+ */
+ protected StringUtilsReplacer() {
+ }
+
+ @Override
+ public String replace( final String text, final Map<String, String> map ) {
+ return replaceEach( text, keys( map ), values( map ) );
+ }
+}
src/main/java/com/keenwrite/processors/text/TextReplacementFactory.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.text;
+
+import java.util.Map;
+
+/**
+ * Used to generate a class capable of efficiently replacing variable
+ * definitions with their values.
+ */
+public final class TextReplacementFactory {
+
+ private static final TextReplacer APACHE = new StringUtilsReplacer();
+ private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
+
+ /**
+ * Returns a text search/replacement instance that is reasonably optimal for
+ * the given length of text.
+ *
+ * @param length The length of text that requires some search and replacing.
+ * @return A class that can search and replace text with utmost expediency.
+ */
+ public static TextReplacer getTextReplacer( final int length ) {
+ // After about 1,500 characters, the StringUtils implementation is less
+ // performant than the Aho-Corsick implementation.
+ //
+ // See http://stackoverflow.com/a/40836618/59087
+ return length < 1500 ? APACHE : AHO_CORASICK;
+ }
+
+ /**
+ * Convenience method to instantiate a suitable text replacer algorithm and
+ * perform a replacement using the given map. At this point, the values should
+ * be already dereferenced and ready to be substituted verbatim; any
+ * recursively defined values must have been interpolated previously.
+ *
+ * @param text The text containing zero or more variables to replace.
+ * @param map The map of variables to their dereferenced values.
+ * @return The text with all variables replaced.
+ */
+ public static String replace(
+ final String text, final Map<String, String> map ) {
+ return getTextReplacer( text.length() ).replace( text, map );
+ }
+}
src/main/java/com/keenwrite/processors/text/TextReplacer.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.processors.text;
+
+import java.util.Map;
+
+/**
+ * Defines the ability to replace text given a set of keys and values.
+ */
+public interface TextReplacer {
+
+ /**
+ * Searches through the given text for any of the keys given in the map and
+ * replaces the keys that appear in the text with the key's corresponding
+ * value.
+ *
+ * @param text The text that contains zero or more keys.
+ * @param map The set of keys mapped to replacement values.
+ * @return The given text with all keys replaced with corresponding values.
+ */
+ String replace( String text, Map<String, String> map );
+}
src/main/java/com/keenwrite/service/Options.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service;
+
+import com.dlsc.preferencesfx.PreferencesFx;
+
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+/**
+ * Responsible for persisting options that are safe to load before the UI
+ * is shown. This can include items like window dimensions, last file
+ * opened, split pane locations, and more. This cannot be used to persist
+ * options that are user-controlled (i.e., all options available through
+ * {@link PreferencesFx}).
+ */
+public interface Options extends Service {
+
+ /**
+ * Returns the {@link Preferences} that persist settings that cannot
+ * be configured via the user interface.
+ *
+ * @return A valid {@link Preferences} instance, never {@code null}.
+ */
+ Preferences getState();
+
+ /**
+ * Stores the key and value into the user preferences to be loaded the next
+ * time the application is launched.
+ *
+ * @param key Name of the key to persist along with its value.
+ * @param value Value to associate with the key.
+ * @throws BackingStoreException Could not persist the change.
+ */
+ void put( String key, String value ) throws BackingStoreException;
+
+ /**
+ * Retrieves the value for a key in the user preferences.
+ *
+ * @param key Retrieve the value of this key.
+ * @param defaultValue The value to return in the event that the given key has
+ * no associated value.
+ * @return The value associated with the key.
+ */
+ String get( String key, String defaultValue );
+
+ /**
+ * Retrieves the value for a key in the user preferences. This will return
+ * the empty string if the value cannot be found.
+ *
+ * @param key The key to find in the preferences.
+ * @return A non-null, possibly empty value for the key.
+ */
+ String get( String key );
+}
src/main/java/com/keenwrite/service/Service.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service;
+
+/**
+ * All services inherit from this one.
+ */
+public interface Service {
+}
src/main/java/com/keenwrite/service/Settings.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Defines how settings and options can be retrieved.
+ */
+public interface Settings extends Service {
+
+ /**
+ * Returns a setting property or its default value.
+ *
+ * @param property The property key name to obtain its value.
+ * @param defaultValue The default value to return iff the property cannot be
+ * found.
+ * @return The property value for the given property key.
+ */
+ String getSetting( String property, String defaultValue );
+
+ /**
+ * Returns a setting property or its default value.
+ *
+ * @param property The property key name to obtain its value.
+ * @param defaultValue The default value to return iff the property cannot be
+ * found.
+ * @return The property value for the given property key.
+ */
+ int getSetting( String property, int defaultValue );
+
+ /**
+ * Returns a list of property names that begin with the given prefix. The
+ * prefix is included in any matching results. This will return keys that
+ * either match the prefix or start with the prefix followed by a dot ('.').
+ * For example, a prefix value of <code>the.property.name</code> will likely
+ * return the expected results, but <code>the.property.name.</code> (note the
+ * extraneous period) will probably not.
+ *
+ * @param prefix The prefix to compare against each property name.
+ * @return The list of property names that have the given prefix.
+ */
+ Iterator<String> getKeys( final String prefix );
+
+ /**
+ * Convert the generic list of property objects into strings.
+ *
+ * @param property The property value to coerce.
+ * @param defaults The defaults values to use should the property be unset.
+ * @return The list of properties coerced from objects to strings.
+ */
+ List<String> getStringSettingList( String property, List<String> defaults );
+
+ /**
+ * Converts the generic list of property objects into strings.
+ *
+ * @param property The property value to coerce.
+ * @return The list of properties coerced from objects to strings.
+ */
+ List<String> getStringSettingList( String property );
+}
src/main/java/com/keenwrite/service/Snitch.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Observer;
+
+/**
+ * Listens for changes to file system files and directories.
+ */
+public interface Snitch extends Service, Runnable {
+
+ /**
+ * Adds an observer to the set of observers for this object, provided that it
+ * is not the same as some observer already in the set. The order in which
+ * notifications will be delivered to multiple observers is not specified.
+ *
+ * @param o The object to receive changed events for when monitored files
+ * are changed.
+ */
+ void addObserver( Observer o );
+
+ /**
+ * Listens for changes to the path. If the path specifies a file, then only
+ * notifications pertaining to that file are sent. Otherwise, change events
+ * for the directory that contains the file are sent. This method must allow
+ * for multiple calls to the same file without incurring additional listeners
+ * or events.
+ *
+ * @param file Send notifications when this file changes, can be null.
+ * @throws IOException Couldn't create a watcher for the given file.
+ */
+ void listen( Path file ) throws IOException;
+
+ /**
+ * Removes the given file from the notifications list.
+ *
+ * @param file The file to stop monitoring for any changes, can be null.
+ */
+ void ignore( Path file );
+
+ /**
+ * Stop listening for events.
+ */
+ void stop();
+}
src/main/java/com/keenwrite/service/events/Notification.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service.events;
+
+/**
+ * Represents a message that contains a title and content.
+ */
+public interface Notification {
+
+ /**
+ * Alert title.
+ *
+ * @return A non-null string to use as alert message title.
+ */
+ String getTitle();
+
+ /**
+ * Alert message content.
+ *
+ * @return A non-null string that contains information for the user.
+ */
+ String getContent();
+}
src/main/java/com/keenwrite/service/events/Notifier.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service.events;
+
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonType;
+import javafx.stage.Window;
+
+/**
+ * Provides the application with a uniform way to notify the user of events.
+ */
+public interface Notifier {
+
+ ButtonType YES = ButtonType.YES;
+ ButtonType NO = ButtonType.NO;
+ ButtonType CANCEL = ButtonType.CANCEL;
+
+ /**
+ * Constructs a default alert message text for a modal alert dialog.
+ *
+ * @param title The dialog box message title.
+ * @param message The dialog box message content (needs formatting).
+ * @param args The arguments to the message content that must be formatted.
+ * @return The message suitable for building a modal alert dialog.
+ */
+ Notification createNotification(
+ String title,
+ String message,
+ Object... args );
+
+ /**
+ * Creates an alert of alert type error with a message showing the cause of
+ * the error.
+ *
+ * @param parent Dialog box owner (for modal purposes).
+ * @param message The error message, title, and possibly more details.
+ * @return A modal alert dialog box ready to display using showAndWait.
+ */
+ Alert createError( Window parent, Notification message );
+
+ /**
+ * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
+ *
+ * @param parent Dialog box owner (for modal purposes).
+ * @param message The message, title, and possibly more details.
+ * @return A modal alert dialog box ready to display using showAndWait.
+ */
+ Alert createConfirmation( Window parent, Notification message );
+}
src/main/java/com/keenwrite/service/events/impl/ButtonOrderPane.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service.events.impl;
+
+import javafx.scene.Node;
+import javafx.scene.control.ButtonBar;
+import javafx.scene.control.DialogPane;
+
+import static com.keenwrite.Constants.SETTINGS;
+import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
+
+/**
+ * Ensures a consistent button order for alert dialogs across platforms (because
+ * the default button order on Linux defies all logic).
+ */
+public class ButtonOrderPane extends DialogPane {
+
+ @Override
+ protected Node createButtonBar() {
+ final var node = (ButtonBar) super.createButtonBar();
+ node.setButtonOrder( getButtonOrder() );
+ return node;
+ }
+
+ private String getButtonOrder() {
+ return getSetting( "dialog.alert.button.order.windows",
+ BUTTON_ORDER_WINDOWS );
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private String getSetting( final String key, final String defaultValue ) {
+ return SETTINGS.getSetting( key, defaultValue );
+ }
+}
src/main/java/com/keenwrite/service/events/impl/DefaultNotification.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service.events.impl;
+
+import com.keenwrite.service.events.Notification;
+
+import java.text.MessageFormat;
+
+/**
+ * Responsible for alerting the user to prominent information.
+ */
+public class DefaultNotification implements Notification {
+
+ private final String title;
+ private final String content;
+
+ /**
+ * Constructs default message text for a notification.
+ *
+ * @param title The message title.
+ * @param message The message content (needs formatting).
+ * @param args The arguments to the message content that must be formatted.
+ */
+ public DefaultNotification(
+ final String title,
+ final String message,
+ final Object... args ) {
+ this.title = title;
+ this.content = MessageFormat.format( message, args );
+ }
+
+ @Override
+ public String getTitle() {
+ return this.title;
+ }
+
+ @Override
+ public String getContent() {
+ return this.content;
+ }
+}
src/main/java/com/keenwrite/service/events/impl/DefaultNotifier.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service.events.impl;
+
+import com.keenwrite.service.events.Notification;
+import com.keenwrite.service.events.Notifier;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.stage.Window;
+
+import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
+import static javafx.scene.control.Alert.AlertType.ERROR;
+
+/**
+ * Provides the ability to notify the user of events that need attention,
+ * such as prompting the user to confirm closing when there are unsaved changes.
+ */
+public final class DefaultNotifier implements Notifier {
+
+ /**
+ * Contains all the information that the user needs to know about a problem.
+ *
+ * @param title The context for the message.
+ * @param message The message content (formatted with the given args).
+ * @param args Parameters for the message content.
+ * @return A notification instance, never null.
+ */
+ @Override
+ public Notification createNotification(
+ final String title,
+ final String message,
+ final Object... args ) {
+ return new DefaultNotification( title, message, args );
+ }
+
+ private Alert createAlertDialog(
+ final Window parent,
+ final AlertType alertType,
+ final Notification message ) {
+
+ final Alert alert = new Alert( alertType );
+
+ alert.setDialogPane( new ButtonOrderPane() );
+ alert.setTitle( message.getTitle() );
+ alert.setHeaderText( null );
+ alert.setContentText( message.getContent() );
+ alert.initOwner( parent );
+
+ return alert;
+ }
+
+ @Override
+ public Alert createConfirmation( final Window parent,
+ final Notification message ) {
+ final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
+
+ alert.getButtonTypes().setAll( YES, NO, CANCEL );
+
+ return alert;
+ }
+
+ @Override
+ public Alert createError( final Window parent, final Notification message ) {
+ return createAlertDialog( parent, ERROR, message );
+ }
+}
src/main/java/com/keenwrite/service/impl/DefaultOptions.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service.impl;
+
+import com.keenwrite.service.Options;
+
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+import static com.keenwrite.Constants.PREFS_ROOT;
+import static com.keenwrite.Constants.PREFS_STATE;
+import static java.util.prefs.Preferences.userRoot;
+
+/**
+ * Persistent options user can change at runtime.
+ */
+public class DefaultOptions implements Options {
+ public DefaultOptions() {
+ }
+
+ /**
+ * This will throw IllegalArgumentException if the value exceeds the maximum
+ * preferences value length.
+ *
+ * @param key The name of the key to associate with the value.
+ * @param value The value to persist.
+ * @throws BackingStoreException New value not persisted.
+ */
+ @Override
+ public void put( final String key, final String value )
+ throws BackingStoreException {
+ getState().put( key, value );
+ getState().flush();
+ }
+
+ @Override
+ public String get( final String key, final String value ) {
+ return getState().get( key, value );
+ }
+
+ @Override
+ public String get( final String key ) {
+ return get( key, "" );
+ }
+
+ private Preferences getRootPreferences() {
+ return userRoot().node( PREFS_ROOT );
+ }
+
+ @Override
+ public Preferences getState() {
+ return getRootPreferences().node( PREFS_STATE );
+ }
+}
src/main/java/com/keenwrite/service/impl/DefaultSettings.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service.impl;
+
+import com.keenwrite.service.Settings;
+import org.apache.commons.configuration2.PropertiesConfiguration;
+import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
+import org.apache.commons.configuration2.convert.ListDelimiterHandler;
+import org.apache.commons.configuration2.ex.ConfigurationException;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.keenwrite.Constants.SETTINGS_NAME;
+
+/**
+ * Responsible for loading settings that help avoid hard-coded assumptions.
+ */
+public class DefaultSettings implements Settings {
+
+ private static final char VALUE_SEPARATOR = ',';
+
+ private PropertiesConfiguration properties;
+
+ public DefaultSettings() throws ConfigurationException {
+ setProperties( createProperties() );
+ }
+
+ /**
+ * Returns the value of a string property.
+ *
+ * @param property The property key.
+ * @param defaultValue The value to return if no property key has been set.
+ * @return The property key value, or defaultValue when no key found.
+ */
+ @Override
+ public String getSetting( final String property, final String defaultValue ) {
+ return getSettings().getString( property, defaultValue );
+ }
+
+ /**
+ * Returns the value of a string property.
+ *
+ * @param property The property key.
+ * @param defaultValue The value to return if no property key has been set.
+ * @return The property key value, or defaultValue when no key found.
+ */
+ @Override
+ public int getSetting( final String property, final int defaultValue ) {
+ return getSettings().getInt( property, defaultValue );
+ }
+
+ /**
+ * Convert the generic list of property objects into strings.
+ *
+ * @param property The property value to coerce.
+ * @param defaults The defaults values to use should the property be unset.
+ * @return The list of properties coerced from objects to strings.
+ */
+ @Override
+ public List<String> getStringSettingList(
+ final String property, final List<String> defaults ) {
+ return getSettings().getList( String.class, property, defaults );
+ }
+
+ /**
+ * Convert a list of property objects into strings, with no default value.
+ *
+ * @param property The property value to coerce.
+ * @return The list of properties coerced from objects to strings.
+ */
+ @Override
+ public List<String> getStringSettingList( final String property ) {
+ return getStringSettingList( property, null );
+ }
+
+ /**
+ * Returns a list of property names that begin with the given prefix.
+ *
+ * @param prefix The prefix to compare against each property name.
+ * @return The list of property names that have the given prefix.
+ */
+ @Override
+ public Iterator<String> getKeys( final String prefix ) {
+ return getSettings().getKeys( prefix );
+ }
+
+ private PropertiesConfiguration createProperties()
+ throws ConfigurationException {
+
+ final URL url = getPropertySource();
+ final PropertiesConfiguration configuration = new PropertiesConfiguration();
+
+ if( url != null ) {
+ try( final Reader r = new InputStreamReader( url.openStream(),
+ getDefaultEncoding() ) ) {
+ configuration.setListDelimiterHandler( createListDelimiterHandler() );
+ configuration.read( r );
+
+ } catch( final IOException ex ) {
+ throw new RuntimeException( new ConfigurationException( ex ) );
+ }
+ }
+
+ return configuration;
+ }
+
+ protected Charset getDefaultEncoding() {
+ return Charset.defaultCharset();
+ }
+
+ protected ListDelimiterHandler createListDelimiterHandler() {
+ return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
+ }
+
+ private URL getPropertySource() {
+ return DefaultSettings.class.getResource( getSettingsFilename() );
+ }
+
+ private String getSettingsFilename() {
+ return SETTINGS_NAME;
+ }
+
+ private void setProperties( final PropertiesConfiguration configuration ) {
+ this.properties = configuration;
+ }
+
+ private PropertiesConfiguration getSettings() {
+ return this.properties;
+ }
+}
src/main/java/com/keenwrite/service/impl/DefaultSnitch.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.service.impl;
+
+import com.keenwrite.service.Snitch;
+
+import java.io.IOException;
+import java.nio.file.*;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Observable;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static com.keenwrite.Constants.APP_WATCHDOG_TIMEOUT;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+
+/**
+ * Listens for file changes. Other classes can register paths to be monitored
+ * and listen for changes to those paths.
+ */
+public class DefaultSnitch extends Observable implements Snitch {
+
+ /**
+ * Service for listening to directories for modifications.
+ */
+ private WatchService watchService;
+
+ /**
+ * Directories being monitored for changes.
+ */
+ private Map<WatchKey, Path> keys;
+
+ /**
+ * Files that will kick off notification events if modified.
+ */
+ private Set<Path> eavesdropped;
+
+ /**
+ * Set to true when running; set to false to stop listening.
+ */
+ private volatile boolean listening;
+
+ public DefaultSnitch() {
+ }
+
+ @Override
+ public void stop() {
+ setListening( false );
+ }
+
+ /**
+ * Adds a listener to the list of files to watch for changes. If the file is
+ * already in the monitored list, this will return immediately.
+ *
+ * @param file Path to a file to watch for changes.
+ * @throws IOException The file could not be monitored.
+ */
+ @Override
+ public void listen( final Path file ) throws IOException {
+ if( file != null && getEavesdropped().add( file ) ) {
+ final Path dir = toDirectory( file );
+ final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
+
+ getWatchMap().put( key, dir );
+ }
+ }
+
+ /**
+ * Returns the given path to a file (or directory) as a directory. If the
+ * given path is already a directory, it is returned. Otherwise, this returns
+ * the directory that contains the file. This will fail if the file is stored
+ * in the root folder.
+ *
+ * @param path The file to return as a directory, which should always be the
+ * case.
+ * @return The given path as a directory, if a file, otherwise the path
+ * itself.
+ */
+ private Path toDirectory( final Path path ) {
+ return Files.isDirectory( path )
+ ? path
+ : path.toFile().getParentFile().toPath();
+ }
+
+ /**
+ * Stop listening to the given file for change events. This fails silently.
+ *
+ * @param file The file to no longer monitor for changes.
+ */
+ @Override
+ public void ignore( final Path file ) {
+ if( file != null ) {
+ final Path directory = toDirectory( file );
+
+ // Remove all occurrences (there should be only one).
+ getWatchMap().values().removeAll( Collections.singleton( directory ) );
+
+ // Remove all occurrences (there can be only one).
+ getEavesdropped().remove( file );
+ }
+ }
+
+ /**
+ * Loops until stop is called, or the application is terminated.
+ */
+ @Override
+ @SuppressWarnings("BusyWait")
+ public void run() {
+ setListening( true );
+
+ while( isListening() ) {
+ try {
+ final WatchKey key = getWatchService().take();
+ final Path path = get( key );
+
+ // Prevent receiving two separate ENTRY_MODIFY events: file modified
+ // and timestamp updated. Instead, receive one ENTRY_MODIFY event
+ // with two counts.
+ Thread.sleep( APP_WATCHDOG_TIMEOUT );
+
+ for( final WatchEvent<?> event : key.pollEvents() ) {
+ final Path changed = path.resolve( (Path) event.context() );
+
+ if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
+ setChanged();
+ notifyObservers( changed );
+ }
+ }
+
+ if( !key.reset() ) {
+ ignore( path );
+ }
+ } catch( final IOException | InterruptedException ex ) {
+ // Stop eavesdropping.
+ setListening( false );
+ }
+ }
+ }
+
+ /**
+ * Returns true if the list of files being listened to for changes contains
+ * the given file.
+ *
+ * @param file Path to a system file.
+ * @return true The given file is being monitored for changes.
+ */
+ private boolean isListening( final Path file ) {
+ return getEavesdropped().contains( file );
+ }
+
+ /**
+ * Returns a path for a given watch key.
+ *
+ * @param key The key to lookup its corresponding path.
+ * @return The path for the given key.
+ */
+ private Path get( final WatchKey key ) {
+ return getWatchMap().get( key );
+ }
+
+ private synchronized Map<WatchKey, Path> getWatchMap() {
+ if( this.keys == null ) {
+ this.keys = createWatchKeys();
+ }
+
+ return this.keys;
+ }
+
+ protected Map<WatchKey, Path> createWatchKeys() {
+ return new ConcurrentHashMap<>();
+ }
+
+ /**
+ * Returns a list of files that, when changed, will kick off a notification.
+ *
+ * @return A non-null, possibly empty, list of files.
+ */
+ private synchronized Set<Path> getEavesdropped() {
+ if( this.eavesdropped == null ) {
+ this.eavesdropped = createEavesdropped();
+ }
+
+ return this.eavesdropped;
+ }
+
+ protected Set<Path> createEavesdropped() {
+ return ConcurrentHashMap.newKeySet();
+ }
+
+ /**
+ * The existing watch service, or a new instance if null.
+ *
+ * @return A valid WatchService instance, never null.
+ * @throws IOException Could not create a new watch service.
+ */
+ private synchronized WatchService getWatchService() throws IOException {
+ if( this.watchService == null ) {
+ this.watchService = createWatchService();
+ }
+
+ return this.watchService;
+ }
+
+ protected WatchService createWatchService() throws IOException {
+ final FileSystem fileSystem = FileSystems.getDefault();
+ return fileSystem.newWatchService();
+ }
+
+ /**
+ * Answers whether the loop should continue executing.
+ *
+ * @return true The internal listening loop should continue listening for file
+ * modification events.
+ */
+ protected boolean isListening() {
+ return this.listening;
+ }
+
+ /**
+ * Requests the snitch to stop eavesdropping on file changes.
+ *
+ * @param listening Use true to indicate the service should stop running.
+ */
+ private void setListening( final boolean listening ) {
+ this.listening = listening;
+ }
+}
src/main/java/com/keenwrite/sigils/RSigilOperator.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.sigils;
+
+import static com.keenwrite.sigils.YamlSigilOperator.KEY_SEPARATOR_DEF;
+
+/**
+ * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils.
+ */
+public class RSigilOperator extends SigilOperator {
+ public static final char KEY_SEPARATOR_R = '$';
+
+ public static final String PREFIX = "`r#";
+ public static final char SUFFIX = '`';
+
+ private final String mDelimiterBegan =
+ getUserPreferences().getRDelimiterBegan();
+ private final String mDelimiterEnded =
+ getUserPreferences().getRDelimiterEnded();
+
+ /**
+ * Returns the given string R-escaping backticks prepended and appended. This
+ * is not null safe. Do not pass null into this method.
+ *
+ * @param key The string to adorn with R token delimiters.
+ * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`".
+ */
+ @Override
+ public String apply( final String key ) {
+ assert key != null;
+
+ return PREFIX
+ + mDelimiterBegan
+ + entoken( key )
+ + mDelimiterEnded
+ + SUFFIX;
+ }
+
+ /**
+ * Transforms a definition key (bracketed by token delimiters) into the
+ * expected format for an R variable key name.
+ *
+ * @param key The variable name to transform, can be empty but not null.
+ * @return The transformed variable name.
+ */
+ public static String entoken( final String key ) {
+ return "v$" +
+ YamlSigilOperator.detoken( key )
+ .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R );
+ }
+}
src/main/java/com/keenwrite/sigils/SigilOperator.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.sigils;
+
+import com.keenwrite.preferences.UserPreferences;
+
+import java.util.function.UnaryOperator;
+
+/**
+ * Responsible for updating definition keys to use a machine-readable format
+ * corresponding to the type of file being edited. This changes a definition
+ * key name based on some criteria determined by the factory that creates
+ * implementations of this interface.
+ */
+public abstract class SigilOperator implements UnaryOperator<String> {
+ protected static UserPreferences getUserPreferences() {
+ return UserPreferences.getInstance();
+ }
+}
src/main/java/com/keenwrite/sigils/YamlSigilOperator.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.sigils;
+
+import java.util.regex.Pattern;
+
+import static java.lang.String.format;
+import static java.util.regex.Pattern.compile;
+import static java.util.regex.Pattern.quote;
+
+/**
+ * Brackets definition keys with token delimiters.
+ */
+public class YamlSigilOperator extends SigilOperator {
+ public static final char KEY_SEPARATOR_DEF = '.';
+
+ private static final String mDelimiterBegan =
+ getUserPreferences().getDefDelimiterBegan();
+ private static final String mDelimiterEnded =
+ getUserPreferences().getDefDelimiterEnded();
+
+ /**
+ * Non-greedy match of key names delimited by definition tokens.
+ */
+ private static final String REGEX =
+ format( "(%s.*?%s)", quote( mDelimiterBegan ), quote( mDelimiterEnded ) );
+
+ /**
+ * Compiled regular expression for matching delimited references.
+ */
+ public static final Pattern REGEX_PATTERN = compile( REGEX );
+
+ /**
+ * Returns the given {@link String} verbatim because variables in YAML
+ * documents and plain Markdown documents already have the appropriate
+ * tokenizable syntax wrapped around the text.
+ *
+ * @param key Returned verbatim.
+ */
+ @Override
+ public String apply( final String key ) {
+ return key;
+ }
+
+ /**
+ * Adds delimiters to the given key.
+ *
+ * @param key The key to adorn with start and stop definition tokens.
+ * @return The given key bracketed by definition token symbols.
+ */
+ public static String entoken( final String key ) {
+ assert key != null;
+ return mDelimiterBegan + key + mDelimiterEnded;
+ }
+
+ /**
+ * Removes start and stop definition key delimiters from the given key. This
+ * method does not check for delimiters, only that there are sufficient
+ * characters to remove from either end of the given key.
+ *
+ * @param key The key adorned with start and stop definition tokens.
+ * @return The given key with the delimiters removed.
+ */
+ public static String detoken( final String key ) {
+ final int beganLen = mDelimiterBegan.length();
+ final int endedLen = mDelimiterEnded.length();
+
+ return key.length() > beganLen + endedLen
+ ? key.substring( beganLen, key.length() - endedLen )
+ : key;
+ }
+}
src/main/java/com/keenwrite/spelling/api/SpellCheckListener.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.spelling.api;
+
+import java.util.function.BiConsumer;
+
+/**
+ * Represents an operation that accepts two input arguments and returns no
+ * result. Unlike most other functional interfaces, this class is expected to
+ * operate via side-effects.
+ * <p>
+ * This is used instead of a {@link BiConsumer} to avoid autoboxing.
+ * </p>
+ */
+@FunctionalInterface
+public interface SpellCheckListener {
+
+ /**
+ * Performs an operation on the given arguments.
+ *
+ * @param text The text associated with a beginning and ending offset.
+ * @param beganOffset A starting offset, used as an index into a string.
+ * @param endedOffset An ending offset, which should equal text.length() +
+ * beganOffset.
+ */
+ void accept( String text, int beganOffset, int endedOffset );
+}
src/main/java/com/keenwrite/spelling/api/SpellChecker.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.spelling.api;
+
+import java.util.List;
+
+/**
+ * Defines the responsibilities for a spell checking API. The intention is
+ * to allow different spell checking implementations to be used by the
+ * application, such as SymSpell and LinSpell.
+ */
+public interface SpellChecker {
+
+ /**
+ * Answers whether the given lexeme, in whole, is found in the lexicon. The
+ * lexicon lookup is performed case-insensitively. This method should be
+ * used instead of {@link #suggestions(String, int)} for performance reasons.
+ *
+ * @param lexeme The word to check for correctness.
+ * @return {@code true} if the lexeme is in the lexicon.
+ */
+ boolean inLexicon( String lexeme );
+
+ /**
+ * Gets a list of spelling corrections for the given lexeme.
+ *
+ * @param lexeme A word to check for correctness that's not in the lexicon.
+ * @param count The maximum number of alternatives to return.
+ * @return A list of words in the lexicon that are similar to the given
+ * lexeme.
+ */
+ List<String> suggestions( String lexeme, int count );
+
+ /**
+ * Iterates over the given text, emitting starting and ending offsets into
+ * the text for every word that is missing from the lexicon.
+ *
+ * @param text The text to check for words missing from the lexicon.
+ * @param consumer Every missing word emits a message with the starting
+ * and ending offset into the text where said word is found.
+ */
+ void proofread( String text, SpellCheckListener consumer );
+}
src/main/java/com/keenwrite/spelling/impl/PermissiveSpeller.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.spelling.impl;
+
+import com.keenwrite.spelling.api.SpellCheckListener;
+import com.keenwrite.spelling.api.SpellChecker;
+
+import java.util.List;
+
+/**
+ * Responsible for spell checking in the event that a real spell checking
+ * implementation cannot be created (for any reason). Does not perform any
+ * spell checking and indicates that any given lexeme is in the lexicon.
+ */
+public class PermissiveSpeller implements SpellChecker {
+ /**
+ * Returns {@code true}, ignoring the given word.
+ *
+ * @param ignored Unused.
+ * @return {@code true}
+ */
+ @Override
+ public boolean inLexicon( final String ignored ) {
+ return true;
+ }
+
+ /**
+ * Returns an array with the given lexeme.
+ *
+ * @param lexeme The word to return.
+ * @param ignored Unused.
+ * @return A suggestion list containing the given lexeme.
+ */
+ @Override
+ public List<String> suggestions( final String lexeme, final int ignored ) {
+ return List.of( lexeme );
+ }
+
+ /**
+ * Performs no action.
+ *
+ * @param text Unused.
+ * @param ignored Uncalled.
+ */
+ @Override
+ public void proofread(
+ final String text, final SpellCheckListener ignored ) {
+ }
+}
src/main/java/com/keenwrite/spelling/impl/SymSpellSpeller.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.spelling.impl;
+
+import com.keenwrite.spelling.api.SpellCheckListener;
+import com.keenwrite.spelling.api.SpellChecker;
+import io.gitlab.rxp90.jsymspell.SuggestItem;
+import io.gitlab.rxp90.jsymspell.SymSpell;
+import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
+
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
+import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
+import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
+import static java.lang.Character.isLetter;
+
+/**
+ * Responsible for spell checking using {@link SymSpell}.
+ */
+public class SymSpellSpeller implements SpellChecker {
+ private final BreakIterator mBreakIterator = BreakIterator.getWordInstance();
+
+ private final SymSpell mSymSpell;
+
+ /**
+ * Creates a new lexicon for the given collection of lexemes.
+ *
+ * @param lexiconWords The words in the lexicon to add for spell checking,
+ * must not be empty.
+ * @return An instance of {@link SpellChecker} that can check if a word
+ * is correct and suggest alternatives.
+ */
+ public static SpellChecker forLexicon(
+ final Collection<String> lexiconWords ) {
+ assert lexiconWords != null && !lexiconWords.isEmpty();
+
+ final SymSpellBuilder builder = new SymSpellBuilder()
+ .setLexiconWords( lexiconWords );
+
+ return new SymSpellSpeller( builder.build() );
+ }
+
+ /**
+ * Prevent direct instantiation so that only the {@link SpellChecker}
+ * interface
+ * is available.
+ *
+ * @param symSpell The implementation-specific spell checker.
+ */
+ private SymSpellSpeller( final SymSpell symSpell ) {
+ mSymSpell = symSpell;
+ }
+
+ @Override
+ public boolean inLexicon( final String lexeme ) {
+ return lookup( lexeme, CLOSEST ).size() == 1;
+ }
+
+ @Override
+ public List<String> suggestions( final String lexeme, int count ) {
+ final List<String> result = new ArrayList<>( count );
+
+ for( final var item : lookup( lexeme, ALL ) ) {
+ if( count-- > 0 ) {
+ result.add( item.getSuggestion() );
+ }
+ else {
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public void proofread(
+ final String text, final SpellCheckListener consumer ) {
+ assert text != null;
+ assert consumer != null;
+
+ mBreakIterator.setText( text );
+
+ int boundaryIndex = mBreakIterator.first();
+ int previousIndex = 0;
+
+ while( boundaryIndex != BreakIterator.DONE ) {
+ final var lex = text.substring( previousIndex, boundaryIndex )
+ .toLowerCase();
+
+ // Get the lexeme for the possessive.
+ final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" );
+ final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex;
+
+ if( isWord( lexeme ) && !inLexicon( lexeme ) ) {
+ consumer.accept( lex, previousIndex, boundaryIndex );
+ }
+
+ previousIndex = boundaryIndex;
+ boundaryIndex = mBreakIterator.next();
+ }
+ }
+
+ /**
+ * Answers whether the given string is likely a word by checking the first
+ * character.
+ *
+ * @param word The word to check.
+ * @return {@code true} if the word begins with a letter.
+ */
+ private boolean isWord( final String word ) {
+ return !word.isEmpty() && isLetter( word.charAt( 0 ) );
+ }
+
+ /**
+ * Returns a list of {@link SuggestItem} instances that provide alternative
+ * spellings for the given lexeme.
+ *
+ * @param lexeme A word to look up in the lexicon.
+ * @param v Influences the number of results returned.
+ * @return Alternative lexemes.
+ */
+ private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
+ return getSpeller().lookup( lexeme, v );
+ }
+
+ private SymSpell getSpeller() {
+ return mSymSpell;
+ }
+}
src/main/java/com/keenwrite/util/Action.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+import de.jensd.fx.glyphs.GlyphIcons;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.input.KeyCombination;
+
+/**
+ * Defines actions the user can take by interacting with the GUI.
+ */
+public class Action {
+ public final String text;
+ public final KeyCombination accelerator;
+ public final GlyphIcons icon;
+ public final EventHandler<ActionEvent> action;
+ public final ObservableBooleanValue disable;
+
+ public Action(
+ final String text,
+ final String accelerator,
+ final GlyphIcons icon,
+ final EventHandler<ActionEvent> action,
+ final ObservableBooleanValue disable ) {
+
+ this.text = text;
+ this.accelerator = accelerator == null ?
+ null : KeyCombination.valueOf( accelerator );
+ this.icon = icon;
+ this.action = action;
+ this.disable = disable;
+ }
+}
src/main/java/com/keenwrite/util/ActionBuilder.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+import com.keenwrite.Messages;
+import de.jensd.fx.glyphs.GlyphIcons;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+
+/**
+ * Provides a fluent interface around constructing actions so that duplication
+ * can be avoided.
+ */
+public class ActionBuilder {
+ private String mText;
+ private String mAccelerator;
+ private GlyphIcons mIcon;
+ private EventHandler<ActionEvent> mAction;
+ private ObservableBooleanValue mDisable;
+
+ /**
+ * Sets the action text based on a resource bundle key.
+ *
+ * @param key The key to look up in the {@link Messages}.
+ * @return The corresponding value, or the key name if none found.
+ */
+ public ActionBuilder setText( final String key ) {
+ mText = Messages.get( key, key );
+ return this;
+ }
+
+ public ActionBuilder setAccelerator( final String accelerator ) {
+ mAccelerator = accelerator;
+ return this;
+ }
+
+ public ActionBuilder setIcon( final GlyphIcons icon ) {
+ mIcon = icon;
+ return this;
+ }
+
+ public ActionBuilder setAction( final EventHandler<ActionEvent> action ) {
+ mAction = action;
+ return this;
+ }
+
+ public ActionBuilder setDisable( final ObservableBooleanValue disable ) {
+ mDisable = disable;
+ return this;
+ }
+
+ public Action build() {
+ return new Action( mText, mAccelerator, mIcon, mAction, mDisable );
+ }
+}
src/main/java/com/keenwrite/util/ActionUtils.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.Separator;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.Tooltip;
+
+/**
+ * Responsible for creating menu items and toolbar buttons.
+ */
+public class ActionUtils {
+
+ public static Menu createMenu( final String text, final Action... actions ) {
+ return new Menu( text, null, createMenuItems( actions ) );
+ }
+
+ public static MenuItem[] createMenuItems( final Action... actions ) {
+ final MenuItem[] menuItems = new MenuItem[ actions.length ];
+
+ for( int i = 0; i < actions.length; i++ ) {
+ menuItems[ i ] = (actions[ i ] == null)
+ ? new SeparatorMenuItem()
+ : createMenuItem( actions[ i ] );
+ }
+
+ return menuItems;
+ }
+
+ public static MenuItem createMenuItem( final Action action ) {
+ final MenuItem menuItem = new MenuItem( action.text );
+
+ if( action.accelerator != null ) {
+ menuItem.setAccelerator( action.accelerator );
+ }
+
+ if( action.icon != null ) {
+ menuItem.setGraphic(
+ FontAwesomeIconFactory.get().createIcon( action.icon ) );
+ }
+
+ menuItem.setOnAction( action.action );
+
+ if( action.disable != null ) {
+ menuItem.disableProperty().bind( action.disable );
+ }
+
+ menuItem.setMnemonicParsing( true );
+
+ return menuItem;
+ }
+
+ public static ToolBar createToolBar( final Action... actions ) {
+ return new ToolBar( createToolBarButtons( actions ) );
+ }
+
+ public static Node[] createToolBarButtons( final Action... actions ) {
+ Node[] buttons = new Node[ actions.length ];
+ for( int i = 0; i < actions.length; i++ ) {
+ buttons[ i ] = (actions[ i ] != null)
+ ? createToolBarButton( actions[ i ] )
+ : new Separator();
+ }
+ return buttons;
+ }
+
+ public static Button createToolBarButton( final Action action ) {
+ final Button button = new Button();
+ button.setGraphic(
+ FontAwesomeIconFactory
+ .get()
+ .createIcon( action.icon, "1.2em" ) );
+
+ String tooltip = action.text;
+
+ if( tooltip.endsWith( "..." ) ) {
+ tooltip = tooltip.substring( 0, tooltip.length() - 3 );
+ }
+
+ if( action.accelerator != null ) {
+ tooltip += " (" + action.accelerator.getDisplayText() + ')';
+ }
+
+ button.setTooltip( new Tooltip( tooltip ) );
+ button.setFocusTraversable( false );
+ button.setOnAction( action.action );
+
+ if( action.disable != null ) {
+ button.disableProperty().bind( action.disable );
+ }
+
+ return button;
+ }
+}
src/main/java/com/keenwrite/util/BoundedCache.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A map that removes the oldest entry once its capacity (cache size) has
+ * been reached.
+ *
+ * @param <K> The type of key mapped to a value.
+ * @param <V> The type of value mapped to a key.
+ */
+public class BoundedCache<K, V> extends LinkedHashMap<K, V> {
+ private final int mCacheSize;
+
+ /**
+ * Constructs a new instance having a finite size.
+ *
+ * @param cacheSize The maximum number of entries.
+ */
+ public BoundedCache( final int cacheSize ) {
+ mCacheSize = cacheSize;
+ }
+
+ @Override
+ protected boolean removeEldestEntry(
+ final Map.Entry<K, V> eldest ) {
+ return size() > mCacheSize;
+ }
+}
src/main/java/com/keenwrite/util/ProtocolResolver.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+
+import static com.keenwrite.util.ProtocolScheme.UNKNOWN;
+
+/**
+ * Responsible for determining the protocol of a resource.
+ */
+public class ProtocolResolver {
+ /**
+ * Returns the protocol for a given URI or filename.
+ *
+ * @param resource Determine the protocol for this URI or filename.
+ * @return The protocol for the given resource.
+ */
+ public static ProtocolScheme getProtocol( final String resource ) {
+ String protocol;
+
+ try {
+ final URI uri = new URI( resource );
+
+ if( uri.isAbsolute() ) {
+ protocol = uri.getScheme();
+ }
+ else {
+ final URL url = new URL( resource );
+ protocol = url.getProtocol();
+ }
+ } catch( final Exception e ) {
+ // Could be HTTP, HTTPS?
+ if( resource.startsWith( "//" ) ) {
+ throw new IllegalArgumentException( "Relative context: " + resource );
+ }
+ else {
+ final File file = new File( resource );
+ protocol = getProtocol( file );
+ }
+ }
+
+ return ProtocolScheme.valueFrom( protocol );
+ }
+
+ /**
+ * Returns the protocol for a given file.
+ *
+ * @param file Determine the protocol for this file.
+ * @return The protocol for the given file.
+ */
+ private static String getProtocol( final File file ) {
+ String result;
+
+ try {
+ result = file.toURI().toURL().getProtocol();
+ } catch( final MalformedURLException ex ) {
+ // Value guaranteed to avoid identification as a standard protocol.
+ result = UNKNOWN.toString();
+ }
+
+ return result;
+ }
+}
src/main/java/com/keenwrite/util/ProtocolScheme.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+/**
+ * Represents the type of data encoding scheme used for a universal resource
+ * indicator.
+ */
+public enum ProtocolScheme {
+ /**
+ * Denotes either HTTP or HTTPS.
+ */
+ HTTP,
+ /**
+ * Denotes a local file.
+ */
+ FILE,
+ /**
+ * Could not determine schema (or is not supported by the application).
+ */
+ UNKNOWN;
+
+ /**
+ * Answers {@code true} if the given protocol is either HTTP or HTTPS.
+ *
+ * @return {@code true} the protocol is either HTTP or HTTPS.
+ */
+ public boolean isHttp() {
+ return this == HTTP;
+ }
+
+ /**
+ * Answers {@code true} if the given protocol is for a local file.
+ *
+ * @return {@code true} the protocol is for a local file reference.
+ */
+ public boolean isFile() {
+ return this == FILE;
+ }
+
+ /**
+ * Determines the protocol scheme for a given string.
+ *
+ * @param protocol A string representing data encoding protocol scheme.
+ * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
+ * valid value from this enumeration.
+ */
+ public static ProtocolScheme valueFrom( String protocol ) {
+ ProtocolScheme result = UNKNOWN;
+ protocol = sanitize( protocol );
+
+ for( final var scheme : values() ) {
+ // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate.
+ if( protocol.startsWith( scheme.name() ) ) {
+ result = scheme;
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns an empty string if the given string to sanitize is {@code null},
+ * otherwise the given string in uppercase. Uppercase is used to align with
+ * the enum name.
+ *
+ * @param s The string to sanitize, may be {@code null}.
+ * @return A non-{@code null} string.
+ */
+ private static String sanitize( final String s ) {
+ return s == null ? "" : s.toUpperCase();
+ }
+}
src/main/java/com/keenwrite/util/ResourceWalker.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.*;
+import java.util.function.Consumer;
+
+import static java.nio.file.FileSystems.newFileSystem;
+import static java.util.Collections.emptyMap;
+
+/**
+ * Responsible for finding file resources.
+ */
+public class ResourceWalker {
+ private static final PathMatcher PATH_MATCHER =
+ FileSystems.getDefault().getPathMatcher( "glob:**.{ttf,otf}" );
+
+ /**
+ * @param dirName The root directory to scan for files matching the glob.
+ * @param c The consumer function to call for each matching path found.
+ * @throws URISyntaxException Could not convert the resource to a URI.
+ * @throws IOException Could not walk the tree.
+ */
+ public static void walk( final String dirName, final Consumer<Path> c )
+ throws URISyntaxException, IOException {
+ final var resource = ResourceWalker.class.getResource( dirName );
+
+ if( resource != null ) {
+ final var uri = resource.toURI();
+ final var path = uri.getScheme().equals( "jar" )
+ ? newFileSystem( uri, emptyMap() ).getPath( dirName )
+ : Paths.get( uri );
+ final var walk = Files.walk( path, 10 );
+
+ for( final var it = walk.iterator(); it.hasNext(); ) {
+ final Path p = it.next();
+ if( PATH_MATCHER.matches( p ) ) {
+ c.accept( p );
+ }
+ }
+ }
+ }
+}
src/main/java/com/keenwrite/util/StageState.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+import java.util.prefs.Preferences;
+
+import javafx.application.Platform;
+import javafx.scene.shape.Rectangle;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+
+/**
+ * Saves and restores Stage state (window bounds, maximized, fullScreen).
+ */
+public class StageState {
+
+ public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
+ public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
+ public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
+
+ private final Stage mStage;
+ private final Preferences mState;
+
+ private Rectangle normalBounds;
+ private boolean runLaterPending;
+
+ public StageState( final Stage stage, final Preferences state ) {
+ mStage = stage;
+ mState = state;
+
+ restore();
+
+ stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
+
+ stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() );
+ stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() );
+ stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() );
+ stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() );
+ }
+
+ private void save() {
+ final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
+
+ if( bounds != null ) {
+ mState.putDouble( "windowX", bounds.getX() );
+ mState.putDouble( "windowY", bounds.getY() );
+ mState.putDouble( "windowWidth", bounds.getWidth() );
+ mState.putDouble( "windowHeight", bounds.getHeight() );
+ }
+
+ mState.putBoolean( "windowMaximized", mStage.isMaximized() );
+ mState.putBoolean( "windowFullScreen", mStage.isFullScreen() );
+ }
+
+ private void restore() {
+ final double x = mState.getDouble( "windowX", Double.NaN );
+ final double y = mState.getDouble( "windowY", Double.NaN );
+ final double w = mState.getDouble( "windowWidth", Double.NaN );
+ final double h = mState.getDouble( "windowHeight", Double.NaN );
+ final boolean maximized = mState.getBoolean( "windowMaximized", false );
+ final boolean fullScreen = mState.getBoolean( "windowFullScreen", false );
+
+ if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
+ mStage.setX( x );
+ mStage.setY( y );
+ } // else: default behavior is center on screen
+
+ if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
+ mStage.setWidth( w );
+ mStage.setHeight( h );
+ } // else: default behavior is use scene size
+
+ if( fullScreen != mStage.isFullScreen() ) {
+ mStage.setFullScreen( fullScreen );
+ }
+
+ if( maximized != mStage.isMaximized() ) {
+ mStage.setMaximized( maximized );
+ }
+ }
+
+ /**
+ * Remembers the window bounds when the window is not iconified, maximized or
+ * in fullScreen.
+ */
+ private void boundsChanged() {
+ // avoid too many (and useless) runLater() invocations
+ if( runLaterPending ) {
+ return;
+ }
+
+ runLaterPending = true;
+
+ // must use runLater() to ensure that change of all properties
+ // (x, y, width, height, iconified, maximized and fullScreen)
+ // has finished
+ Platform.runLater( () -> {
+ runLaterPending = false;
+
+ if( isNormalState() ) {
+ normalBounds = getStageBounds();
+ }
+ } );
+ }
+
+ private boolean isNormalState() {
+ return !mStage.isIconified() &&
+ !mStage.isMaximized() &&
+ !mStage.isFullScreen();
+ }
+
+ private Rectangle getStageBounds() {
+ return new Rectangle(
+ mStage.getX(),
+ mStage.getY(),
+ mStage.getWidth(),
+ mStage.getHeight()
+ );
+ }
+}
src/main/java/com/keenwrite/util/Utils.java
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.util;
+
+import java.util.ArrayList;
+import java.util.prefs.Preferences;
+
+/**
+ * Responsible for trimming, storing, and retrieving strings.
+ */
+public class Utils {
+
+ public static String ltrim( final String s ) {
+ int i = 0;
+
+ while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) {
+ i++;
+ }
+
+ return s.substring( i );
+ }
+
+ public static String rtrim( final String s ) {
+ int i = s.length() - 1;
+
+ while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) {
+ i--;
+ }
+
+ return s.substring( 0, i + 1 );
+ }
+
+ public static String[] getPrefsStrings( final Preferences prefs,
+ String key ) {
+ final ArrayList<String> arr = new ArrayList<>( 256 );
+
+ for( int i = 0; i < 10000; i++ ) {
+ final String s = prefs.get( key + (i + 1), null );
+
+ if( s == null ) {
+ break;
+ }
+
+ arr.add( s );
+ }
+
+ return arr.toArray( new String[ 0 ] );
+ }
+
+ public static void putPrefsStrings( Preferences prefs, String key,
+ String[] strings ) {
+ for( int i = 0; i < strings.length; i++ ) {
+ prefs.put( key + (i + 1), strings[ i ] );
+ }
+
+ for( int i = strings.length; prefs.get( key + (i + 1),
+ null ) != null; i++ ) {
+ prefs.remove( key + (i + 1) );
+ }
+ }
+}
src/main/java/com/scrivenvar/AbstractFileFactory.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.scrivenvar.service.Settings;
-import com.scrivenvar.util.ProtocolScheme;
-
-import java.nio.file.Path;
-
-import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
-import static com.scrivenvar.Constants.SETTINGS;
-import static com.scrivenvar.FileType.UNKNOWN;
-import static com.scrivenvar.predicates.PredicateFactory.createFileTypePredicate;
-import static java.lang.String.format;
-
-/**
- * Provides common behaviours for factories that instantiate classes based on
- * file type.
- */
-public class AbstractFileFactory {
-
- private static final String MSG_UNKNOWN_FILE_TYPE =
- "Unknown type '%s' for file '%s'.";
-
- /**
- * Determines the file type from the path extension. This should only be
- * called when it is known that the file type won't be a definition file
- * (e.g., YAML or other definition source), but rather an editable file
- * (e.g., Markdown, XML, etc.).
- *
- * @param path The path with a file name extension.
- * @return The FileType for the given path.
- */
- public FileType lookup( final Path path ) {
- return lookup( path, GLOB_PREFIX_FILE );
- }
-
- /**
- * Creates a file type that corresponds to the given path.
- *
- * @param path Reference to a variable definition file.
- * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
- * @return The file type that corresponds to the given path.
- */
- protected FileType lookup( final Path path, final String prefix ) {
- assert path != null;
- assert prefix != null;
-
- final var settings = getSettings();
- final var keys = settings.getKeys( prefix );
-
- var found = false;
- var fileType = UNKNOWN;
-
- while( keys.hasNext() && !found ) {
- final var key = keys.next();
- final var patterns = settings.getStringSettingList( key );
- final var predicate = createFileTypePredicate( patterns );
-
- if( found = predicate.test( path.toFile() ) ) {
- // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
- // to a standard name (as defined in the settings.properties file).
- final String suffix = key.replace( prefix + ".", "" );
- fileType = FileType.from( suffix );
- }
- }
-
- return fileType;
- }
-
- /**
- * Throws IllegalArgumentException because the given path could not be
- * recognized. This exists because
- *
- * @param type The detected path type (protocol, file extension, etc.).
- * @param path The path to a source of definitions.
- */
- protected void unknownFileType(
- final ProtocolScheme type, final String path ) {
- final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
- throw new IllegalArgumentException( msg );
- }
-
- /**
- * Return the singleton Settings instance.
- *
- * @return A non-null instance.
- */
- private Settings getSettings() {
- return SETTINGS;
- }
-}
src/main/java/com/scrivenvar/Constants.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.scrivenvar.service.Settings;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-/**
- * Defines application-wide default values.
- */
-public class Constants {
-
- public static final Settings SETTINGS = Services.load( Settings.class );
-
- /**
- * Prevent instantiation.
- */
- private Constants() {
- }
-
- private static String get( final String key ) {
- return SETTINGS.getSetting( key, "" );
- }
-
- @SuppressWarnings("SameParameterValue")
- private static int get( final String key, final int defaultValue ) {
- return SETTINGS.getSetting( key, defaultValue );
- }
-
- // Bootstrapping...
- public static final String SETTINGS_NAME =
- "/com/scrivenvar/settings.properties";
-
- public static final String DEFINITION_NAME = "variables.yaml";
-
- public static final String APP_TITLE = get( "application.title" );
- public static final String APP_BUNDLE_NAME = get( "application.messages" );
-
- // Prevent double events when updating files on Linux (save and timestamp).
- public static final int APP_WATCHDOG_TIMEOUT = get(
- "application.watchdog.timeout", 200 );
-
- public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
- public static final String STYLESHEET_MARKDOWN = get(
- "file.stylesheet.markdown" );
- public static final String STYLESHEET_PREVIEW = get(
- "file.stylesheet.preview" );
-
- public static final String FILE_LOGO_16 = get( "file.logo.16" );
- public static final String FILE_LOGO_32 = get( "file.logo.32" );
- public static final String FILE_LOGO_128 = get( "file.logo.128" );
- public static final String FILE_LOGO_256 = get( "file.logo.256" );
- public static final String FILE_LOGO_512 = get( "file.logo.512" );
-
- public static final String PREFS_ROOT = get( "preferences.root" );
- public static final String PREFS_STATE = get( "preferences.root.state" );
-
- /**
- * Refer to filename extension settings in the configuration file. Do not
- * terminate these prefixes with a period.
- */
- public static final String GLOB_PREFIX_FILE = "file.ext";
- public static final String GLOB_PREFIX_DEFINITION =
- "definition." + GLOB_PREFIX_FILE;
-
- /**
- * Three parameters: line number, column number, and offset.
- */
- public static final String STATUS_BAR_LINE = "Main.status.line";
-
- public static final String STATUS_BAR_OK = "Main.status.state.default";
-
- /**
- * Used to show an error while parsing, usually syntactical.
- */
- public static final String STATUS_PARSE_ERROR = "Main.status.error.parse";
- public static final String STATUS_DEFINITION_BLANK = "Main.status.error.def.blank";
- public static final String STATUS_DEFINITION_EMPTY = "Main.status.error.def.empty";
-
- /**
- * One parameter: the word under the cursor that could not be found.
- */
- public static final String STATUS_DEFINITION_MISSING = "Main.status.error.def.missing";
-
- /**
- * Used when creating flat maps relating to resolved variables.
- */
- public static final int DEFAULT_MAP_SIZE = 64;
-
- /**
- * Default image extension order to use when scanning.
- */
- public static final String PERSIST_IMAGES_DEFAULT =
- get( "file.ext.image.order" );
-
- /**
- * Default working directory to use for R startup script.
- */
- public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
-
- /**
- * Default path to use for an untitled (pathless) file.
- */
- public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY );
-
- /**
- * Default starting delimiter for definition variables.
- */
- public static final String DEF_DELIM_BEGAN_DEFAULT = "${";
-
- /**
- * Default ending delimiter for definition variables.
- */
- public static final String DEF_DELIM_ENDED_DEFAULT = "}";
-
- /**
- * Default starting delimiter when inserting R variables.
- */
- public static final String R_DELIM_BEGAN_DEFAULT = "x( ";
-
- /**
- * Default ending delimiter when inserting R variables.
- */
- public static final String R_DELIM_ENDED_DEFAULT = " )";
-
- /**
- * Resource directory where different language lexicons are located.
- */
- public static final String LEXICONS_DIRECTORY = "lexicons";
-
- /**
- * Used as the prefix for uniquely identifying HTML block elements, which
- * helps coordinate scrolling the preview pane to where the user is typing.
- */
- public static final String PARAGRAPH_ID_PREFIX = "p-";
-
- /**
- * Absolute location of true type font files within the Java archive file.
- */
- public static final String FONT_DIRECTORY = "/fonts";
-
- /**
- * Default text editor font size, in points.
- */
- public static final float FONT_SIZE_EDITOR = 12f;
-}
src/main/java/com/scrivenvar/FileEditorTab.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.scrivenvar.editors.EditorPane;
-import com.scrivenvar.editors.markdown.MarkdownEditorPane;
-import com.scrivenvar.service.events.Notification;
-import com.scrivenvar.service.events.Notifier;
-import javafx.beans.binding.Bindings;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.ReadOnlyBooleanProperty;
-import javafx.beans.property.ReadOnlyBooleanWrapper;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.event.EventType;
-import javafx.scene.Scene;
-import javafx.scene.control.Tab;
-import javafx.scene.control.Tooltip;
-import javafx.scene.text.Text;
-import javafx.stage.Window;
-import org.fxmisc.flowless.VirtualizedScrollPane;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import org.fxmisc.undo.UndoManager;
-import org.jetbrains.annotations.NotNull;
-import org.mozilla.universalchardet.UniversalDetector;
-
-import java.io.File;
-import java.nio.charset.Charset;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import static com.scrivenvar.Messages.get;
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static com.scrivenvar.StatusBarNotifier.getNotifier;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Locale.ENGLISH;
-import static javafx.application.Platform.runLater;
-
-/**
- * Editor for a single file.
- */
-public final class FileEditorTab extends Tab {
-
- private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane();
-
- private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
- private final BooleanProperty canUndo = new SimpleBooleanProperty();
- private final BooleanProperty canRedo = new SimpleBooleanProperty();
-
- /**
- * Character encoding used by the file (or default encoding if none found).
- */
- private Charset mEncoding = UTF_8;
-
- /**
- * File to load into the editor.
- */
- private Path mPath;
-
- public FileEditorTab( final Path path ) {
- setPath( path );
-
- mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
-
- setOnSelectionChanged( e -> {
- if( isSelected() ) {
- runLater( this::activated );
- requestFocus();
- }
- } );
- }
-
- private void updateTab() {
- setText( getTabTitle() );
- setGraphic( getModifiedMark() );
- setTooltip( getTabTooltip() );
- }
-
- /**
- * Returns the base filename (without the directory names).
- *
- * @return The untitled text if the path hasn't been set.
- */
- private String getTabTitle() {
- return getPath().getFileName().toString();
- }
-
- /**
- * Returns the full filename represented by the path.
- *
- * @return The untitled text if the path hasn't been set.
- */
- private Tooltip getTabTooltip() {
- final Path filePath = getPath();
- return new Tooltip( filePath == null ? "" : filePath.toString() );
- }
-
- /**
- * Returns a marker to indicate whether the file has been modified.
- *
- * @return "*" when the file has changed; otherwise null.
- */
- private Text getModifiedMark() {
- return isModified() ? new Text( "*" ) : null;
- }
-
- /**
- * Called when the user switches tab.
- */
- private void activated() {
- // Tab is closed or no longer active.
- if( getTabPane() == null || !isSelected() ) {
- return;
- }
-
- // If the tab is devoid of content, load it.
- if( getContent() == null ) {
- readFile();
- initLayout();
- initUndoManager();
- }
- }
-
- private void initLayout() {
- setContent( getScrollPane() );
- }
-
- /**
- * Tracks undo requests, but can only be called <em>after</em> load.
- */
- private void initUndoManager() {
- final UndoManager<?> undoManager = getUndoManager();
- undoManager.forgetHistory();
-
- // Bind the editor undo manager to the properties.
- mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
- canUndo.bind( undoManager.undoAvailableProperty() );
- canRedo.bind( undoManager.redoAvailableProperty() );
- }
-
- private void requestFocus() {
- getEditorPane().requestFocus();
- }
-
- /**
- * Searches from the caret position forward for the given string.
- *
- * @param needle The text string to match.
- */
- public void searchNext( final String needle ) {
- final String haystack = getEditorText();
- int index = haystack.indexOf( needle, getCaretPosition() );
-
- // Wrap around.
- if( index == -1 ) {
- index = haystack.indexOf( needle );
- }
-
- if( index >= 0 ) {
- setCaretPosition( index );
- getEditor().selectRange( index, index + needle.length() );
- }
- }
-
- /**
- * Gets a reference to the scroll pane that houses the editor.
- *
- * @return The editor's scroll pane, containing a vertical scrollbar.
- */
- public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
- return getEditorPane().getScrollPane();
- }
-
- /**
- * Returns the index into the text where the caret blinks happily away.
- *
- * @return A number from 0 to the editor's document text length.
- */
- public int getCaretPosition() {
- return getEditor().getCaretPosition();
- }
-
- /**
- * Moves the caret to a given offset.
- *
- * @param offset The new caret offset.
- */
- private void setCaretPosition( final int offset ) {
- getEditor().moveTo( offset );
- getEditor().requestFollowCaret();
- }
-
- /**
- * Returns the text area associated with this tab.
- *
- * @return A text editor.
- */
- private StyleClassedTextArea getEditor() {
- return getEditorPane().getEditor();
- }
-
- /**
- * Returns true if the given path exactly matches this tab's path.
- *
- * @param check The path to compare against.
- * @return true The paths are the same.
- */
- public boolean isPath( final Path check ) {
- final Path filePath = getPath();
-
- return filePath != null && filePath.equals( check );
- }
-
- /**
- * Reads the entire file contents from the path associated with this tab.
- */
- private void readFile() {
- final Path path = getPath();
- final File file = path.toFile();
-
- try {
- if( file.exists() ) {
- if( file.canWrite() && file.canRead() ) {
- final EditorPane pane = getEditorPane();
- pane.setText( asString( Files.readAllBytes( path ) ) );
- pane.scrollToTop();
- }
- else {
- final String msg = get( "FileEditor.loadFailed.reason.permissions" );
- alert( "FileEditor.loadFailed.message", file.toString(), msg );
- }
- }
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
-
- /**
- * Saves the entire file contents from the path associated with this tab.
- *
- * @return true The file has been saved.
- */
- public boolean save() {
- try {
- final EditorPane editor = getEditorPane();
- Files.write( getPath(), asBytes( editor.getText() ) );
- editor.getUndoManager().mark();
- return true;
- } catch( final Exception ex ) {
- return popupAlert(
- "FileEditor.saveFailed.title",
- "FileEditor.saveFailed.message",
- ex
- );
- }
- }
-
- /**
- * Creates an alert dialog and waits for it to close.
- *
- * @param titleKey Resource bundle key for the alert dialog title.
- * @param messageKey Resource bundle key for the alert dialog message.
- * @param e The unexpected happening.
- * @return false
- */
- @SuppressWarnings("SameParameterValue")
- private boolean popupAlert(
- final String titleKey, final String messageKey, final Exception e ) {
- final Notifier service = getNotifier();
- final Path filePath = getPath();
-
- final Notification message = service.createNotification(
- get( titleKey ),
- get( messageKey ),
- filePath == null ? "" : filePath,
- e.getMessage()
- );
-
- try {
- service.createError( getWindow(), message ).showAndWait();
- } catch( final Exception ex ) {
- alert( ex );
- }
-
- return false;
- }
-
- private Window getWindow() {
- final Scene scene = getEditorPane().getScene();
-
- if( scene == null ) {
- throw new UnsupportedOperationException( "No scene window available" );
- }
-
- return scene.getWindow();
- }
-
- /**
- * Returns a best guess at the file encoding. If the encoding could not be
- * detected, this will return the default charset for the JVM.
- *
- * @param bytes The bytes to perform character encoding detection.
- * @return The character encoding.
- */
- private Charset detectEncoding( final byte[] bytes ) {
- final var detector = new UniversalDetector( null );
- detector.handleData( bytes, 0, bytes.length );
- detector.dataEnd();
-
- final String charset = detector.getDetectedCharset();
-
- return charset == null
- ? Charset.defaultCharset()
- : Charset.forName( charset.toUpperCase( ENGLISH ) );
- }
-
- /**
- * Converts the given string to an array of bytes using the encoding that was
- * originally detected (if any) and associated with this file.
- *
- * @param text The text to convert into the original file encoding.
- * @return A series of bytes ready for writing to a file.
- */
- private byte[] asBytes( final String text ) {
- return text.getBytes( getEncoding() );
- }
-
- /**
- * Converts the given bytes into a Java String. This will call setEncoding
- * with the encoding detected by the CharsetDetector.
- *
- * @param text The text of unknown character encoding.
- * @return The text, in its auto-detected encoding, as a String.
- */
- private String asString( final byte[] text ) {
- setEncoding( detectEncoding( text ) );
- return new String( text, getEncoding() );
- }
-
- /**
- * Returns the path to the file being edited in this tab.
- *
- * @return A non-null instance.
- */
- public Path getPath() {
- return mPath;
- }
-
- /**
- * Sets the path to a file for editing and then updates the tab with the
- * file contents.
- *
- * @param path A non-null instance.
- */
- public void setPath( final Path path ) {
- assert path != null;
- mPath = path;
-
- updateTab();
- }
-
- public boolean isModified() {
- return mModified.get();
- }
-
- ReadOnlyBooleanProperty modifiedProperty() {
- return mModified.getReadOnlyProperty();
- }
-
- BooleanProperty canUndoProperty() {
- return this.canUndo;
- }
-
- BooleanProperty canRedoProperty() {
- return this.canRedo;
- }
-
- private UndoManager<?> getUndoManager() {
- return getEditorPane().getUndoManager();
- }
-
- /**
- * Forwards to the editor pane's listeners for text change events.
- *
- * @param listener The listener to notify when the text changes.
- */
- public void addTextChangeListener( final ChangeListener<String> listener ) {
- getEditorPane().addTextChangeListener( listener );
- }
-
- /**
- * Forwards to the editor pane's listeners for caret change events.
- *
- * @param listener Notified when the caret position changes.
- */
- public void addCaretPositionListener(
- final ChangeListener<? super Integer> listener ) {
- getEditorPane().addCaretPositionListener( listener );
- }
-
- /**
- * Forwards to the editor pane's listeners for paragraph index change events.
- *
- * @param listener Notified when the caret's paragraph index changes.
- */
- public void addCaretParagraphListener(
- final ChangeListener<? super Integer> listener ) {
- getEditorPane().addCaretParagraphListener( listener );
- }
-
- public <T extends Event> void addEventFilter(
- final EventType<T> eventType,
- final EventHandler<? super T> eventFilter ) {
- getEditor().addEventFilter( eventType, eventFilter );
- }
-
- /**
- * Forwards the request to the editor pane.
- *
- * @return The text to process.
- */
- public String getEditorText() {
- return getEditorPane().getText();
- }
-
- /**
- * Returns the editor pane, or creates one if it doesn't yet exist.
- *
- * @return The editor pane, never null.
- */
- @NotNull
- public MarkdownEditorPane getEditorPane() {
- return mEditorPane;
- }
-
- /**
- * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
- * determined.
- *
- * @return The file encoding or UTF-8 if unknown.
- */
- private Charset getEncoding() {
- return mEncoding;
- }
-
- private void setEncoding( final Charset encoding ) {
- assert encoding != null;
- mEncoding = encoding;
- }
-
- /**
- * Returns the tab title, without any modified indicators.
- *
- * @return The tab title.
- */
- @Override
- public String toString() {
- return getTabTitle();
- }
-}
src/main/java/com/scrivenvar/FileEditorTabPane.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.scrivenvar.service.Options;
-import com.scrivenvar.service.Settings;
-import com.scrivenvar.service.events.Notification;
-import com.scrivenvar.service.events.Notifier;
-import com.scrivenvar.util.Utils;
-import javafx.beans.property.ReadOnlyBooleanProperty;
-import javafx.beans.property.ReadOnlyBooleanWrapper;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.ReadOnlyObjectWrapper;
-import javafx.beans.value.ChangeListener;
-import javafx.collections.ListChangeListener;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.scene.control.Alert;
-import javafx.scene.control.ButtonType;
-import javafx.scene.control.Tab;
-import javafx.scene.control.TabPane;
-import javafx.stage.FileChooser;
-import javafx.stage.FileChooser.ExtensionFilter;
-import javafx.stage.Window;
-
-import java.io.File;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.prefs.Preferences;
-import java.util.stream.Collectors;
-
-import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
-import static com.scrivenvar.Constants.SETTINGS;
-import static com.scrivenvar.FileType.*;
-import static com.scrivenvar.Messages.get;
-import static com.scrivenvar.predicates.PredicateFactory.createFileTypePredicate;
-import static com.scrivenvar.service.events.Notifier.YES;
-
-/**
- * Tab pane for file editors.
- */
-public final class FileEditorTabPane extends TabPane {
-
- private static final String FILTER_EXTENSION_TITLES =
- "Dialog.file.choose.filter";
-
- private static final Options sOptions = Services.load( Options.class );
- private static final Notifier sNotifier = Services.load( Notifier.class );
-
- private final ReadOnlyObjectWrapper<Path> mOpenDefinition =
- new ReadOnlyObjectWrapper<>();
- private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
- new ReadOnlyObjectWrapper<>();
- private final ReadOnlyBooleanWrapper mAnyFileEditorModified =
- new ReadOnlyBooleanWrapper();
- private final ChangeListener<Integer> mCaretPositionListener;
- private final ChangeListener<Integer> mCaretParagraphListener;
-
- /**
- * Constructs a new file editor tab pane.
- *
- * @param caretPositionListener Listens for changes to caret position so
- * that the status bar can update.
- * @param caretParagraphListener Listens for changes to the caret's paragraph
- * so that scrolling may occur.
- */
- public FileEditorTabPane(
- final ChangeListener<Integer> caretPositionListener,
- final ChangeListener<Integer> caretParagraphListener ) {
- final ObservableList<Tab> tabs = getTabs();
-
- setFocusTraversable( false );
- setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
-
- addTabSelectionListener(
- ( tabPane, oldTab, newTab ) -> {
- if( newTab != null ) {
- mActiveFileEditor.set( (FileEditorTab) newTab );
- }
- }
- );
-
- final ChangeListener<Boolean> modifiedListener =
- ( observable, oldValue, newValue ) -> {
- for( final Tab tab : tabs ) {
- if( ((FileEditorTab) tab).isModified() ) {
- mAnyFileEditorModified.set( true );
- break;
- }
- }
- };
-
- tabs.addListener(
- (ListChangeListener<Tab>) change -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- change.getAddedSubList().forEach(
- ( tab ) -> {
- final var fet = (FileEditorTab) tab;
- fet.modifiedProperty().addListener( modifiedListener );
- } );
- }
- else if( change.wasRemoved() ) {
- change.getRemoved().forEach(
- ( tab ) -> {
- final var fet = (FileEditorTab) tab;
- fet.modifiedProperty().removeListener( modifiedListener );
- }
- );
- }
- }
-
- // Changes in the tabs may also change anyFileEditorModified property
- // (e.g. closed modified file)
- modifiedListener.changed( null, null, null );
- }
- );
-
- mCaretPositionListener = caretPositionListener;
- mCaretParagraphListener = caretParagraphListener;
- }
-
- /**
- * Allows observers to be notified when the current file editor tab changes.
- *
- * @param listener The listener to notify of tab change events.
- */
- public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
- // Observe the tab so that when a new tab is opened or selected,
- // a notification is kicked off.
- getSelectionModel().selectedItemProperty().addListener( listener );
- }
-
- /**
- * Returns the tab that has keyboard focus.
- *
- * @return A non-null instance.
- */
- public FileEditorTab getActiveFileEditor() {
- return mActiveFileEditor.get();
- }
-
- /**
- * Returns the property corresponding to the tab that has focus.
- *
- * @return A non-null instance.
- */
- public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
- return mActiveFileEditor.getReadOnlyProperty();
- }
-
- /**
- * Property that can answer whether the text has been modified.
- *
- * @return A non-null instance, true meaning the content has not been saved.
- */
- ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
- return mAnyFileEditorModified.getReadOnlyProperty();
- }
-
- /**
- * Creates a new editor instance from the given path.
- *
- * @param path The file to open.
- * @return A non-null instance.
- */
- private FileEditorTab createFileEditor( final Path path ) {
- assert path != null;
-
- final FileEditorTab tab = new FileEditorTab( path );
-
- tab.setOnCloseRequest( e -> {
- if( !canCloseEditor( tab ) ) {
- e.consume();
- }
- else if( isActiveFileEditor( tab ) ) {
- // Prevent prompting the user to save when there are no file editor
- // tabs open.
- mActiveFileEditor.set( null );
- }
- } );
-
- tab.addCaretPositionListener( mCaretPositionListener );
- tab.addCaretParagraphListener( mCaretParagraphListener );
-
- return tab;
- }
-
- private boolean isActiveFileEditor( final FileEditorTab tab ) {
- return getActiveFileEditor() == tab;
- }
-
- private Path getDefaultPath() {
- final String filename = getDefaultFilename();
- return (new File( filename )).toPath();
- }
-
- private String getDefaultFilename() {
- return getSettings().getSetting( "file.default", "untitled.md" );
- }
-
- /**
- * Called to add a new {@link FileEditorTab} to the tab pane.
- */
- void newEditor() {
- final FileEditorTab tab = createFileEditor( getDefaultPath() );
-
- getTabs().add( tab );
- getSelectionModel().select( tab );
- }
-
- void openFileDialog() {
- final String title = get( "Dialog.file.choose.open.title" );
- final FileChooser dialog = createFileChooser( title );
- final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
-
- if( files != null ) {
- openFiles( files );
- }
- }
-
- /**
- * Opens the files into new editors, unless one of those files was a
- * definition file. The definition file is loaded into the definition pane,
- * but only the first one selected (multiple definition files will result in a
- * warning).
- *
- * @param files The list of non-definition files that the were requested to
- * open.
- */
- private void openFiles( final List<File> files ) {
- final List<String> extensions =
- createExtensionFilter( DEFINITION ).getExtensions();
- final var predicate = createFileTypePredicate( extensions );
-
- // The user might have opened multiple definitions files. These will
- // be discarded from the text editable files.
- final var definitions
- = files.stream().filter( predicate ).collect( Collectors.toList() );
-
- // Create a modifiable list to remove any definition files that were
- // opened.
- final var editors = new ArrayList<>( files );
-
- if( !editors.isEmpty() ) {
- saveLastDirectory( editors.get( 0 ) );
- }
-
- editors.removeAll( definitions );
-
- // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
- if( !editors.isEmpty() ) {
- openEditors( editors, 0 );
- }
-
- if( !definitions.isEmpty() ) {
- openDefinition( definitions.get( 0 ) );
- }
- }
-
- private void openEditors( final List<File> files, final int activeIndex ) {
- final int fileTally = files.size();
- final List<Tab> tabs = getTabs();
-
- // Close single unmodified "Untitled" tab.
- if( tabs.size() == 1 ) {
- final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
-
- if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
- closeEditor( fileEditor, false );
- }
- }
-
- for( int i = 0; i < fileTally; i++ ) {
- final Path path = files.get( i ).toPath();
-
- FileEditorTab fileEditorTab = findEditor( path );
-
- // Only open new files.
- if( fileEditorTab == null ) {
- fileEditorTab = createFileEditor( path );
- getTabs().add( fileEditorTab );
- }
-
- // Select the first file in the list.
- if( i == activeIndex ) {
- getSelectionModel().select( fileEditorTab );
- }
- }
- }
-
- /**
- * Returns a property that changes when a new definition file is opened.
- *
- * @return The path to a definition file that was opened.
- */
- public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
- return getOnOpenDefinitionFile().getReadOnlyProperty();
- }
-
- private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
- return mOpenDefinition;
- }
-
- /**
- * Called when the user has opened a definition file (using the file open
- * dialog box). This will replace the current set of definitions for the
- * active tab.
- *
- * @param definition The file to open.
- */
- private void openDefinition( final File definition ) {
- // TODO: Prevent reading this file twice when a new text document is opened.
- // (might be a matter of checking the value first).
- getOnOpenDefinitionFile().set( definition.toPath() );
- }
-
- /**
- * Called when the contents of the editor are to be saved.
- *
- * @param tab The tab containing content to save.
- * @return true The contents were saved (or needn't be saved).
- */
- public boolean saveEditor( final FileEditorTab tab ) {
- if( tab == null || !tab.isModified() ) {
- return true;
- }
-
- return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
- }
-
- /**
- * Opens the Save As dialog for the user to save the content under a new
- * path.
- *
- * @param tab The tab with contents to save.
- * @return true The contents were saved, or the tab was null.
- */
- public boolean saveEditorAs( final FileEditorTab tab ) {
- if( tab == null ) {
- return true;
- }
-
- getSelectionModel().select( tab );
-
- final FileChooser fileChooser = createFileChooser( get(
- "Dialog.file.choose.save.title" ) );
- final File file = fileChooser.showSaveDialog( getWindow() );
- if( file == null ) {
- return false;
- }
-
- saveLastDirectory( file );
- tab.setPath( file.toPath() );
-
- return tab.save();
- }
-
- void saveAllEditors() {
- for( final FileEditorTab fileEditor : getAllEditors() ) {
- saveEditor( fileEditor );
- }
- }
-
- /**
- * Answers whether the file has had modifications. '
- *
- * @param tab THe tab to check for modifications.
- * @return false The file is unmodified.
- */
- @SuppressWarnings("BooleanMethodIsAlwaysInverted")
- boolean canCloseEditor( final FileEditorTab tab ) {
- final AtomicReference<Boolean> canClose = new AtomicReference<>();
- canClose.set( true );
-
- if( tab.isModified() ) {
- final Notification message = getNotifyService().createNotification(
- Messages.get( "Alert.file.close.title" ),
- Messages.get( "Alert.file.close.text" ),
- tab.getText()
- );
-
- final Alert confirmSave = getNotifyService().createConfirmation(
- getWindow(), message );
-
- final Optional<ButtonType> buttonType = confirmSave.showAndWait();
-
- buttonType.ifPresent(
- save -> canClose.set(
- save == YES ? saveEditor( tab ) : save == ButtonType.NO
- )
- );
- }
-
- return canClose.get();
- }
-
- boolean closeEditor( final FileEditorTab tab, final boolean save ) {
- if( tab == null ) {
- return true;
- }
-
- if( save ) {
- Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
- Event.fireEvent( tab, event );
-
- if( event.isConsumed() ) {
- return false;
- }
- }
-
- getTabs().remove( tab );
-
- if( tab.getOnClosed() != null ) {
- Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
- }
-
- return true;
- }
-
- boolean closeAllEditors() {
- final FileEditorTab[] allEditors = getAllEditors();
- final FileEditorTab activeEditor = getActiveFileEditor();
-
- // try to save active tab first because in case the user decides to cancel,
- // then it stays active
- if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
- return false;
- }
-
- // This should be called any time a tab changes.
- persistPreferences();
-
- // save modified tabs
- for( int i = 0; i < allEditors.length; i++ ) {
- final FileEditorTab fileEditor = allEditors[ i ];
-
- if( fileEditor == activeEditor ) {
- continue;
- }
-
- if( fileEditor.isModified() ) {
- // activate the modified tab to make its modified content visible to
- // the user
- getSelectionModel().select( i );
-
- if( !canCloseEditor( fileEditor ) ) {
- return false;
- }
- }
- }
-
- // Close all tabs.
- for( final FileEditorTab fileEditor : allEditors ) {
- if( !closeEditor( fileEditor, false ) ) {
- return false;
- }
- }
-
- return getTabs().isEmpty();
- }
-
- private FileEditorTab[] getAllEditors() {
- final ObservableList<Tab> tabs = getTabs();
- final int length = tabs.size();
- final FileEditorTab[] allEditors = new FileEditorTab[ length ];
-
- for( int i = 0; i < length; i++ ) {
- allEditors[ i ] = (FileEditorTab) tabs.get( i );
- }
-
- return allEditors;
- }
-
- /**
- * Returns the file editor tab that has the given path.
- *
- * @return null No file editor tab for the given path was found.
- */
- private FileEditorTab findEditor( final Path path ) {
- for( final Tab tab : getTabs() ) {
- final FileEditorTab fileEditor = (FileEditorTab) tab;
-
- if( fileEditor.isPath( path ) ) {
- return fileEditor;
- }
- }
-
- return null;
- }
-
- private FileChooser createFileChooser( String title ) {
- final FileChooser fileChooser = new FileChooser();
-
- fileChooser.setTitle( title );
- fileChooser.getExtensionFilters().addAll(
- createExtensionFilters() );
-
- final String lastDirectory = getPreferences().get( "lastDirectory", null );
- File file = new File( (lastDirectory != null) ? lastDirectory : "." );
-
- if( !file.isDirectory() ) {
- file = new File( "." );
- }
-
- fileChooser.setInitialDirectory( file );
- return fileChooser;
- }
-
- private List<ExtensionFilter> createExtensionFilters() {
- final List<ExtensionFilter> list = new ArrayList<>();
-
- // TODO: Return a list of all properties that match the filter prefix.
- // This will allow dynamic filters to be added and removed just by
- // updating the properties file.
- list.add( createExtensionFilter( ALL ) );
- list.add( createExtensionFilter( SOURCE ) );
- list.add( createExtensionFilter( DEFINITION ) );
- list.add( createExtensionFilter( XML ) );
- return list;
- }
-
- /**
- * Returns a filter for file name extensions recognized by the application
- * that can be opened by the user.
- *
- * @param filetype Used to find the globbing pattern for extensions.
- * @return A filename filter suitable for use by a FileDialog instance.
- */
- private ExtensionFilter createExtensionFilter( final FileType filetype ) {
- final String tKey = String.format( "%s.title.%s",
- FILTER_EXTENSION_TITLES,
- filetype );
- final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
-
- return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
- }
-
- private void saveLastDirectory( final File file ) {
- getPreferences().put( "lastDirectory", file.getParent() );
- }
-
- public void initPreferences() {
- int activeIndex = 0;
-
- final Preferences preferences = getPreferences();
- final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
- final String activeFileName = preferences.get( "activeFile", null );
-
- final List<File> files = new ArrayList<>( fileNames.length );
-
- for( final String fileName : fileNames ) {
- final File file = new File( fileName );
-
- if( file.exists() ) {
- files.add( file );
-
- if( fileName.equals( activeFileName ) ) {
- activeIndex = files.size() - 1;
- }
- }
- }
-
- if( files.isEmpty() ) {
- newEditor();
- }
- else {
- openEditors( files, activeIndex );
- }
- }
-
- public void persistPreferences() {
- final var allEditors = getTabs();
- final List<String> fileNames = new ArrayList<>( allEditors.size() );
-
- for( final var tab : allEditors ) {
- final var fileEditor = (FileEditorTab) tab;
- final var filePath = fileEditor.getPath();
-
- if( filePath != null ) {
- fileNames.add( filePath.toString() );
- }
- }
-
- final var preferences = getPreferences();
- Utils.putPrefsStrings( preferences,
- "file",
- fileNames.toArray( new String[ 0 ] ) );
-
- final var activeEditor = getActiveFileEditor();
- final var filePath = activeEditor == null ? null : activeEditor.getPath();
-
- if( filePath == null ) {
- preferences.remove( "activeFile" );
- }
- else {
- preferences.put( "activeFile", filePath.toString() );
- }
- }
-
- private List<String> getExtensions( final String key ) {
- return getSettings().getStringSettingList( key );
- }
-
- private Notifier getNotifyService() {
- return sNotifier;
- }
-
- private Settings getSettings() {
- return SETTINGS;
- }
-
- protected Options getOptions() {
- return sOptions;
- }
-
- private Window getWindow() {
- return getScene().getWindow();
- }
-
- private Preferences getPreferences() {
- return getOptions().getState();
- }
-}
src/main/java/com/scrivenvar/FileType.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-/**
- * Represents different file type classifications. These are high-level mappings
- * that correspond to the list of glob patterns found within {@code
- * settings.properties}.
- */
-public enum FileType {
-
- ALL( "all" ),
- RMARKDOWN( "rmarkdown" ),
- RXML( "rxml" ),
- SOURCE( "source" ),
- DEFINITION( "definition" ),
- XML( "xml" ),
- CSV( "csv" ),
- JSON( "json" ),
- TOML( "toml" ),
- YAML( "yaml" ),
- PROPERTIES( "properties" ),
- UNKNOWN( "unknown" );
-
- private final String mType;
-
- /**
- * Default constructor for enumerated file type.
- *
- * @param type Human-readable name for the file type.
- */
- FileType( final String type ) {
- mType = type;
- }
-
- /**
- * Returns the file type that corresponds to the given string.
- *
- * @param type The string to compare against this enumeration of file types.
- * @return The corresponding File Type for the given string.
- * @throws IllegalArgumentException Type not found.
- */
- public static FileType from( final String type ) {
- for( final FileType fileType : FileType.values() ) {
- if( fileType.isType( type ) ) {
- return fileType;
- }
- }
-
- throw new IllegalArgumentException( type );
- }
-
- /**
- * Answers whether this file type matches the given string, case insensitive
- * comparison.
- *
- * @param type Presumably a file name extension to check against.
- * @return true The given extension corresponds to this enumerated type.
- */
- public boolean isType( final String type ) {
- return getType().equalsIgnoreCase( type );
- }
-
- /**
- * Returns the human-readable name for the file type.
- *
- * @return A non-null instance.
- */
- private String getType() {
- return mType;
- }
-
- /**
- * Returns the lowercase version of the file name extension.
- *
- * @return The file name, in lower case.
- */
- @Override
- public String toString() {
- return getType();
- }
-}
src/main/java/com/scrivenvar/Launcher.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Calendar;
-import java.util.Properties;
-
-import static java.lang.String.format;
-
-/**
- * Launches the application using the {@link Main} class.
- *
- * <p>
- * This is required until modules are implemented, which may never happen
- * because the application should be ported away from Java and JavaFX.
- * </p>
- */
-public class Launcher {
- /**
- * Delegates to the application entry point.
- *
- * @param args Command-line arguments.
- */
- public static void main( final String[] args ) throws IOException {
- showAppInfo();
- Main.main( args );
- }
-
- @SuppressWarnings("RedundantStringFormatCall")
- private static void showAppInfo() throws IOException {
- out( format( "%s version %s", getTitle(), getVersion() ) );
- out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) );
- out( format( "Portions copyright 2020 Karl Tauber." ) );
- }
-
- private static void out( final String s ) {
- System.out.println( s );
- }
-
- private static String getTitle() throws IOException {
- final Properties properties = loadProperties( "messages.properties" );
- return properties.getProperty( "Main.title" );
- }
-
- private static String getVersion() throws IOException {
- final Properties properties = loadProperties( "app.properties" );
- return properties.getProperty( "application.version" );
- }
-
- private static String getYear() {
- return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
- }
-
- @SuppressWarnings("SameParameterValue")
- private static Properties loadProperties( final String resource )
- throws IOException {
- final Properties properties = new Properties();
- properties.load( getResourceAsStream( getResourceName( resource ) ) );
- return properties;
- }
-
- private static String getResourceName( final String resource ) {
- return format( "%s/%s", getPackagePath(), resource );
- }
-
- private static String getPackagePath() {
- return Launcher.class.getPackageName().replace( '.', '/' );
- }
-
- private static InputStream getResourceAsStream( final String resource ) {
- return Launcher.class.getClassLoader().getResourceAsStream( resource );
- }
-}
src/main/java/com/scrivenvar/Main.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.scrivenvar.preferences.FilePreferencesFactory;
-import com.scrivenvar.service.Options;
-import com.scrivenvar.service.Snitch;
-import com.scrivenvar.util.ResourceWalker;
-import com.scrivenvar.util.StageState;
-import javafx.application.Application;
-import javafx.scene.Scene;
-import javafx.scene.image.Image;
-import javafx.stage.Stage;
-
-import java.awt.*;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.util.Map;
-import java.util.logging.LogManager;
-
-import static com.scrivenvar.Constants.*;
-import static com.scrivenvar.Messages.get;
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment;
-import static java.awt.font.TextAttribute.*;
-import static javafx.scene.input.KeyCode.F11;
-import static javafx.scene.input.KeyEvent.KEY_PRESSED;
-
-/**
- * Application entry point. The application allows users to edit Markdown
- * files and see a real-time preview of the edits.
- */
-public final class Main extends Application {
-
- static {
- // Suppress logging to standard output.
- LogManager.getLogManager().reset();
-
- // Suppress logging to standard error.
- System.err.close();
- }
-
- private final Options mOptions = Services.load( Options.class );
- private final Snitch mSnitch = Services.load( Snitch.class );
-
- private final Thread mSnitchThread = new Thread( getSnitch() );
- private final MainWindow mMainWindow = new MainWindow();
-
- @SuppressWarnings({"FieldCanBeLocal", "unused"})
- private StageState mStageState;
-
- /**
- * Application entry point.
- *
- * @param args Command-line arguments.
- */
- public static void main( final String[] args ) {
- initPreferences();
- initFonts();
- launch( args );
- }
-
- /**
- * JavaFX entry point.
- *
- * @param stage The primary application stage.
- */
- @Override
- public void start( final Stage stage ) {
- initState( stage );
- initStage( stage );
- initSnitch();
-
- stage.show();
-
- // After the stage is visible, the panel dimensions are
- // known, which allows scaling images to fit the preview panel.
- getMainWindow().init();
- }
-
- /**
- * This needs to run before the windowing system kicks in, otherwise the
- * fonts will not be found.
- */
- @SuppressWarnings({"rawtypes", "unchecked"})
- public static void initFonts() {
- final var ge = getLocalGraphicsEnvironment();
-
- try {
- ResourceWalker.walk(
- FONT_DIRECTORY, path -> {
- final var uri = path.toUri();
- final var filename = path.toString();
-
- try( final var is = openFont( uri, filename ) ) {
- final var font = Font.createFont( Font.TRUETYPE_FONT, is );
- final Map attributes = font.getAttributes();
-
- attributes.put( LIGATURES, LIGATURES_ON );
- attributes.put( KERNING, KERNING_ON );
- ge.registerFont( font.deriveFont( attributes ) );
- } catch( final Exception e ) {
- alert( e );
- }
- }
- );
- } catch( final Exception e ) {
- alert( e );
- }
- }
-
- private static InputStream openFont( final URI uri, final String filename )
- throws IOException {
- return uri.getScheme().equals( "jar" )
- ? Main.class.getResourceAsStream( filename )
- : new FileInputStream( filename );
- }
-
- /**
- * Sets the factory used for reading user preferences.
- */
- private static void initPreferences() {
- System.setProperty(
- "java.util.prefs.PreferencesFactory",
- FilePreferencesFactory.class.getName()
- );
- }
-
- private void initState( final Stage stage ) {
- mStageState = new StageState( stage, getOptions().getState() );
- }
-
- private void initStage( final Stage stage ) {
- stage.getIcons().addAll(
- createImage( FILE_LOGO_16 ),
- createImage( FILE_LOGO_32 ),
- createImage( FILE_LOGO_128 ),
- createImage( FILE_LOGO_256 ),
- createImage( FILE_LOGO_512 ) );
- stage.setTitle( getApplicationTitle() );
- stage.setScene( getScene() );
-
- stage.addEventHandler( KEY_PRESSED, event -> {
- if( F11.equals( event.getCode() ) ) {
- stage.setFullScreen( !stage.isFullScreen() );
- }
- } );
- }
-
- /**
- * Watch for file system changes.
- */
- private void initSnitch() {
- getSnitchThread().start();
- }
-
- /**
- * Stops the snitch service, if its running.
- *
- * @throws InterruptedException Couldn't stop the snitch thread.
- */
- @Override
- public void stop() throws InterruptedException {
- getSnitch().stop();
-
- final Thread thread = getSnitchThread();
- thread.interrupt();
- thread.join();
- }
-
- private Snitch getSnitch() {
- return mSnitch;
- }
-
- private Thread getSnitchThread() {
- return mSnitchThread;
- }
-
- private Options getOptions() {
- return mOptions;
- }
-
- private MainWindow getMainWindow() {
- return mMainWindow;
- }
-
- private Scene getScene() {
- return getMainWindow().getScene();
- }
-
- private String getApplicationTitle() {
- return get( "Main.title" );
- }
-
- private Image createImage( final String filename ) {
- return new Image( filename );
- }
-}
src/main/java/com/scrivenvar/MainWindow.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.dlsc.preferencesfx.PreferencesFxEvent;
-import com.scrivenvar.definition.DefinitionFactory;
-import com.scrivenvar.definition.DefinitionPane;
-import com.scrivenvar.definition.DefinitionSource;
-import com.scrivenvar.definition.MapInterpolator;
-import com.scrivenvar.definition.yaml.YamlDefinitionSource;
-import com.scrivenvar.editors.DefinitionNameInjector;
-import com.scrivenvar.editors.EditorPane;
-import com.scrivenvar.editors.markdown.MarkdownEditorPane;
-import com.scrivenvar.preferences.UserPreferences;
-import com.scrivenvar.preview.HTMLPreviewPane;
-import com.scrivenvar.processors.HtmlPreviewProcessor;
-import com.scrivenvar.processors.Processor;
-import com.scrivenvar.processors.ProcessorFactory;
-import com.scrivenvar.service.Options;
-import com.scrivenvar.service.Snitch;
-import com.scrivenvar.spelling.api.SpellCheckListener;
-import com.scrivenvar.spelling.api.SpellChecker;
-import com.scrivenvar.spelling.impl.PermissiveSpeller;
-import com.scrivenvar.spelling.impl.SymSpellSpeller;
-import com.scrivenvar.util.Action;
-import com.scrivenvar.util.ActionBuilder;
-import com.scrivenvar.util.ActionUtils;
-import com.vladsch.flexmark.parser.Parser;
-import com.vladsch.flexmark.util.ast.NodeVisitor;
-import com.vladsch.flexmark.util.ast.VisitHandler;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ListChangeListener.Change;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.*;
-import javafx.scene.control.Alert.AlertType;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import javafx.scene.input.Clipboard;
-import javafx.scene.input.ClipboardContent;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.scene.text.Text;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-import javafx.util.Duration;
-import org.apache.commons.lang3.SystemUtils;
-import org.controlsfx.control.StatusBar;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import org.fxmisc.richtext.model.StyleSpansBuilder;
-import org.reactfx.value.Val;
-
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
-import java.io.InputStreamReader;
-import java.nio.file.Path;
-import java.util.*;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.prefs.Preferences;
-import java.util.stream.Collectors;
-
-import static com.scrivenvar.Constants.*;
-import static com.scrivenvar.Messages.get;
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static com.scrivenvar.util.StageState.*;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singleton;
-import static javafx.application.Platform.runLater;
-import static javafx.event.Event.fireEvent;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- */
-public class MainWindow implements Observer {
- /**
- * The {@code OPTIONS} variable must be declared before all other variables
- * to prevent subsequent initializations from failing due to missing user
- * preferences.
- */
- private static final Options sOptions = Services.load( Options.class );
- private static final Snitch SNITCH = Services.load( Snitch.class );
-
- private final Scene mScene;
- private final StatusBar mStatusBar;
- private final Text mLineNumberText;
- private final TextField mFindTextField;
- private final SpellChecker mSpellChecker;
-
- private final Object mMutex = new Object();
-
- /**
- * Prevents re-instantiation of processing classes.
- */
- private final Map<FileEditorTab, Processor<String>> mProcessors =
- new HashMap<>();
-
- private final Map<String, String> mResolvedMap =
- new HashMap<>( DEFAULT_MAP_SIZE );
-
- private final EventHandler<PreferencesFxEvent> mRPreferencesListener =
- event -> rerender();
-
- /**
- * Called when the definition data is changed.
- */
- private final EventHandler<TreeItem.TreeModificationEvent<Event>>
- mTreeHandler = event -> {
- exportDefinitions( getDefinitionPath() );
- interpolateResolvedMap();
- rerender();
- };
-
- /**
- * Called to inject the selected item when the user presses ENTER in the
- * definition pane.
- */
- private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
- event -> {
- if( event.getCode() == ENTER ) {
- getDefinitionNameInjector().injectSelectedItem();
- }
- };
-
- private final ChangeListener<Integer> mCaretPositionListener =
- ( observable, oldPosition, newPosition ) -> {
- final FileEditorTab tab = getActiveFileEditorTab();
- final EditorPane pane = tab.getEditorPane();
- final StyleClassedTextArea editor = pane.getEditor();
-
- getLineNumberText().setText(
- get( STATUS_BAR_LINE,
- editor.getCurrentParagraph() + 1,
- editor.getParagraphs().size(),
- editor.getCaretPosition()
- )
- );
- };
-
- private final ChangeListener<Integer> mCaretParagraphListener =
- ( observable, oldIndex, newIndex ) ->
- scrollToParagraph( newIndex, true );
-
- private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
- private final DefinitionPane mDefinitionPane = createDefinitionPane();
- private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
- private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
- mCaretPositionListener,
- mCaretParagraphListener );
-
- /**
- * Listens on the definition pane for double-click events.
- */
- private final DefinitionNameInjector mDefinitionNameInjector
- = new DefinitionNameInjector( mDefinitionPane );
-
- public MainWindow() {
- mStatusBar = createStatusBar();
- mLineNumberText = createLineNumberText();
- mFindTextField = createFindTextField();
- mScene = createScene();
- mSpellChecker = createSpellChecker();
-
- // Add the close request listener before the window is shown.
- initLayout();
- StatusBarNotifier.setStatusBar( mStatusBar );
- }
-
- /**
- * Called after the stage is shown.
- */
- public void init() {
- initFindInput();
- initSnitch();
- initDefinitionListener();
- initTabAddedListener();
- initTabChangedListener();
- initPreferences();
- initVariableNameInjector();
- }
-
- private void initLayout() {
- final var scene = getScene();
-
- scene.getStylesheets().add( STYLESHEET_SCENE );
- scene.windowProperty().addListener(
- ( unused, oldWindow, newWindow ) ->
- newWindow.setOnCloseRequest(
- e -> {
- if( !getFileEditorPane().closeAllEditors() ) {
- e.consume();
- }
- }
- )
- );
- }
-
- /**
- * Initialize the find input text field to listen on F3, ENTER, and
- * ESCAPE key presses.
- */
- private void initFindInput() {
- final TextField input = getFindTextField();
-
- input.setOnKeyPressed( ( KeyEvent event ) -> {
- switch( event.getCode() ) {
- case F3:
- case ENTER:
- editFindNext();
- break;
- case F:
- if( !event.isControlDown() ) {
- break;
- }
- case ESCAPE:
- getStatusBar().setGraphic( null );
- getActiveFileEditorTab().getEditorPane().requestFocus();
- break;
- }
- } );
-
- // Remove when the input field loses focus.
- input.focusedProperty().addListener(
- ( focused, oldFocus, newFocus ) -> {
- if( !newFocus ) {
- getStatusBar().setGraphic( null );
- }
- }
- );
- }
-
- /**
- * Watch for changes to external files. In particular, this awaits
- * modifications to any XSL files associated with XML files being edited.
- * When
- * an XSL file is modified (external to the application), the snitch's ears
- * perk up and the file is reloaded. This keeps the XSL transformation up to
- * date with what's on the file system.
- */
- private void initSnitch() {
- SNITCH.addObserver( this );
- }
-
- /**
- * Listen for {@link FileEditorTabPane} to receive open definition file
- * event.
- */
- private void initDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- ( final ObservableValue<? extends Path> file,
- final Path oldPath, final Path newPath ) -> {
- openDefinitions( newPath );
- rerender();
- }
- );
- }
-
- /**
- * Re-instantiates all processors then re-renders the active tab. This
- * will refresh the resolved map, force R to re-initialize, and brute-force
- * XSLT file reloads.
- */
- private void rerender() {
- runLater(
- () -> {
- resetProcessors();
- renderActiveTab();
- }
- );
- }
-
- /**
- * When tabs are added, hook the various change listeners onto the new
- * tab sothat the preview pane refreshes as necessary.
- */
- private void initTabAddedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Make sure the text processor kicks off when new files are opened.
- final ObservableList<Tab> tabs = editorPane.getTabs();
-
- // Update the preview pane on tab changes.
- tabs.addListener(
- ( final Change<? extends Tab> change ) -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- // Multiple tabs can be added simultaneously.
- for( final Tab newTab : change.getAddedSubList() ) {
- final FileEditorTab tab = (FileEditorTab) newTab;
-
- initTextChangeListener( tab );
- initScrollEventListener( tab );
- initSpellCheckListener( tab );
-// initSyntaxListener( tab );
- }
- }
- }
- }
- );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener(
- ( __, ov, nv ) -> {
- process( tab );
- scrollToParagraph( getCurrentParagraphIndex() );
- }
- );
- }
-
- private void initScrollEventListener( final FileEditorTab tab ) {
- final var scrollPane = tab.getScrollPane();
- final var scrollBar = getPreviewPane().getVerticalScrollBar();
-
- addShowListener( scrollPane, ( __ ) -> {
- final var handler = new ScrollEventHandler( scrollPane, scrollBar );
- handler.enabledProperty().bind( tab.selectedProperty() );
- } );
- }
-
- /**
- * Listen for changes to the any particular paragraph and perform a quick
- * spell check upon it. The style classes in the editor will be changed to
- * mark any spelling mistakes in the paragraph. The user may then interact
- * with any misspelled word (i.e., any piece of text that is marked) to
- * revise the spelling.
- *
- * @param tab The tab to spellcheck.
- */
- private void initSpellCheckListener( final FileEditorTab tab ) {
- final var editor = tab.getEditorPane().getEditor();
-
- // When the editor first appears, run a full spell check. This allows
- // spell checking while typing to be restricted to the active paragraph,
- // which is usually substantially smaller than the whole document.
- addShowListener(
- editor, ( __ ) -> spellcheck( editor, editor.getText() )
- );
-
- // Use the plain text changes so that notifications of style changes
- // are suppressed. Checking against the identity ensures that only
- // new text additions or deletions trigger proofreading.
- editor.plainTextChanges()
- .filter( p -> !p.isIdentity() ).subscribe( change -> {
-
- // Only perform a spell check on the current paragraph. The
- // entire document is processed once, when opened.
- final var offset = change.getPosition();
- final var position = editor.offsetToPosition( offset, Forward );
- final var paraId = position.getMajor();
- final var paragraph = editor.getParagraph( paraId );
- final var text = paragraph.getText();
-
- // Ensure that styles aren't doubled-up.
- editor.clearStyle( paraId );
-
- spellcheck( editor, text, paraId );
- } );
- }
-
- /**
- * Listen for new tab selection events.
- */
- private void initTabChangedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabSelectionListener(
- ( tabPane, oldTab, newTab ) -> {
- if( newTab == null ) {
- // Clear the preview pane when closing an editor. When the last
- // tab is closed, this ensures that the preview pane is empty.
- getPreviewPane().clear();
- }
- else {
- final var tab = (FileEditorTab) newTab;
- updateVariableNameInjector( tab );
- process( tab );
- }
- }
- );
- }
-
- /**
- * Reloads the preferences from the previous session.
- */
- private void initPreferences() {
- initDefinitionPane();
- getFileEditorPane().initPreferences();
- getUserPreferences().addSaveEventHandler( mRPreferencesListener );
- }
-
- private void initVariableNameInjector() {
- updateVariableNameInjector( getActiveFileEditorTab() );
- }
-
- /**
- * Calls the listener when the given node is shown for the first time. The
- * visible property is not the same as the initial showing event; visibility
- * can be triggered numerous times (such as going off screen).
- * <p>
- * This is called, for example, before the drag handler can be attached,
- * because the scrollbar for the text editor pane must be visible.
- * </p>
- *
- * @param node The node to watch for showing.
- * @param consumer The consumer to invoke when the event fires.
- */
- private void addShowListener(
- final Node node, final Consumer<Void> consumer ) {
- final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
- runLater( () -> {
- if( newShow != null && newShow ) {
- try {
- consumer.accept( null );
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
- } );
-
- Val.flatMap( node.sceneProperty(), Scene::windowProperty )
- .flatMap( Window::showingProperty )
- .addListener( listener );
- }
-
- private void scrollToParagraph( final int id ) {
- scrollToParagraph( id, false );
- }
-
- /**
- * @param id The paragraph to scroll to, will be approximated if it doesn't
- * exist.
- * @param force {@code true} means to force scrolling immediately, which
- * should only be attempted when it is known that the document
- * has been fully rendered. Otherwise the internal map of ID
- * attributes will be incomplete and scrolling will flounder.
- */
- private void scrollToParagraph( final int id, final boolean force ) {
- synchronized( mMutex ) {
- final var previewPane = getPreviewPane();
- final var scrollPane = previewPane.getScrollPane();
- final int approxId = getActiveEditorPane().approximateParagraphId( id );
-
- if( force ) {
- previewPane.scrollTo( approxId );
- }
- else {
- previewPane.tryScrollTo( approxId );
- }
-
- scrollPane.repaint();
- }
- }
-
- private void updateVariableNameInjector( final FileEditorTab tab ) {
- getDefinitionNameInjector().addListener( tab );
- }
-
- /**
- * Called whenever the preview pane becomes out of sync with the file editor
- * tab. This can be called when the text changes, the caret paragraph
- * changes, or the file tab changes.
- *
- * @param tab The file editor tab that has been changed in some fashion.
- */
- private void process( final FileEditorTab tab ) {
- if( tab != null ) {
- getPreviewPane().setPath( tab.getPath() );
-
- final Processor<String> processor = getProcessors().computeIfAbsent(
- tab, p -> createProcessors( tab )
- );
-
- try {
- processChain( processor, tab.getEditorText() );
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
- }
-
- /**
- * Executes the processing chain, operating on the given string.
- *
- * @param handler The first processor in the chain to call.
- * @param text The initial value of the text to process.
- * @return The final value of the text that was processed by the chain.
- */
- private String processChain( Processor<String> handler, String text ) {
- while( handler != null && text != null ) {
- text = handler.apply( text );
- handler = handler.next();
- }
-
- return text;
- }
-
- private void renderActiveTab() {
- process( getActiveFileEditorTab() );
- }
-
- /**
- * Called when a definition source is opened.
- *
- * @param path Path to the definition source that was opened.
- */
- private void openDefinitions( final Path path ) {
- try {
- final var ds = createDefinitionSource( path );
- setDefinitionSource( ds );
-
- final var prefs = getUserPreferences();
- prefs.definitionPathProperty().setValue( path.toFile() );
- prefs.save();
-
- final var tooltipPath = new Tooltip( path.toString() );
- tooltipPath.setShowDelay( Duration.millis( 200 ) );
-
- final var pane = getDefinitionPane();
- pane.update( ds );
- pane.addTreeChangeHandler( mTreeHandler );
- pane.addKeyEventHandler( mDefinitionKeyHandler );
- pane.filenameProperty().setValue( path.getFileName().toString() );
- pane.setTooltip( tooltipPath );
-
- interpolateResolvedMap();
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
-
- private void exportDefinitions( final Path path ) {
- try {
- final var pane = getDefinitionPane();
- final var root = pane.getTreeView().getRoot();
- final var problemChild = pane.isTreeWellFormed();
-
- if( problemChild == null ) {
- getDefinitionSource().getTreeAdapter().export( root, path );
- }
- else {
- alert( "yaml.error.tree.form", problemChild.getValue() );
- }
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
-
- private void interpolateResolvedMap() {
- final var treeMap = getDefinitionPane().toMap();
- final var map = new HashMap<>( treeMap );
- MapInterpolator.interpolate( map );
-
- getResolvedMap().clear();
- getResolvedMap().putAll( map );
- }
-
- private void initDefinitionPane() {
- openDefinitions( getDefinitionPath() );
- }
-
- //---- File actions -------------------------------------------------------
-
- /**
- * Called when an {@link Observable} instance has changed. This is called
- * by both the {@link Snitch} service and the notify service. The @link
- * Snitch} service can be called for different file types, including
- * {@link DefinitionSource} instances.
- *
- * @param observable The observed instance.
- * @param value The noteworthy item.
- */
- @Override
- public void update( final Observable observable, final Object value ) {
- if( value instanceof Path && observable instanceof Snitch ) {
- updateSelectedTab();
- }
- }
-
- /**
- * Called when a file has been modified.
- */
- private void updateSelectedTab() {
- rerender();
- }
-
- /**
- * After resetting the processors, they will refresh anew to be up-to-date
- * with the files (text and definition) currently loaded into the editor.
- */
- private void resetProcessors() {
- getProcessors().clear();
- }
-
- //---- File actions -------------------------------------------------------
-
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
- }
-
- /**
- * TODO: Upon closing, first remove the tab change listeners. (There's no
- * need to re-render each tab when all are being closed.)
- */
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditorTab() );
- }
-
- private void fileSaveAs() {
- final FileEditorTab editor = getActiveFileEditorTab();
- getFileEditorPane().saveEditorAs( editor );
- getProcessors().remove( editor );
-
- try {
- process( editor );
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Edit actions -------------------------------------------------------
-
- /**
- * Transform the Markdown into HTML then copy that HTML into the copy
- * buffer.
- */
- private void copyHtml() {
- final var markdown = getActiveEditorPane().getText();
- final var processors = createProcessorFactory().createProcessors(
- getActiveFileEditorTab()
- );
-
- final var chain = processors.remove( HtmlPreviewProcessor.class );
-
- final String html = processChain( chain, markdown );
-
- final Clipboard clipboard = Clipboard.getSystemClipboard();
- final ClipboardContent content = new ClipboardContent();
- content.putString( html );
- clipboard.setContent( content );
- }
-
- /**
- * Used to find text in the active file editor window.
- */
- private void editFind() {
- final TextField input = getFindTextField();
- getStatusBar().setGraphic( input );
- input.requestFocus();
- }
-
- public void editFindNext() {
- getActiveFileEditorTab().searchNext( getFindTextField().getText() );
- }
-
- public void editPreferences() {
- getUserPreferences().show();
- }
-
- //---- Insert actions -----------------------------------------------------
-
- /**
- * Delegates to the active editor to handle wrapping the current text
- * selection with leading and trailing strings.
- *
- * @param leading The string to put before the selection.
- * @param trailing The string to put after the selection.
- */
- private void insertMarkdown(
- final String leading, final String trailing ) {
- getActiveEditorPane().surroundSelection( leading, trailing );
- }
-
- private void insertMarkdown(
- final String leading, final String trailing, final String hint ) {
- getActiveEditorPane().surroundSelection( leading, trailing, hint );
- }
-
- //---- View actions -------------------------------------------------------
-
- private void viewRefresh() {
- rerender();
- }
-
- //---- Help actions -------------------------------------------------------
-
- private void helpAbout() {
- final Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( get( "Dialog.about.title" ) );
- alert.setHeaderText( get( "Dialog.about.header" ) );
- alert.setContentText( get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- //---- Member creators ----------------------------------------------------
-
- private SpellChecker createSpellChecker() {
- try {
- final Collection<String> lexicon = readLexicon( "en.txt" );
- return SymSpellSpeller.forLexicon( lexicon );
- } catch( final Exception ex ) {
- alert( ex );
- return new PermissiveSpeller();
- }
- }
-
- /**
- * Factory to create processors that are suited to different file types.
- *
- * @param tab The tab that is subjected to processing.
- * @return A processor suited to the file type specified by the tab's path.
- */
- private Processor<String> createProcessors( final FileEditorTab tab ) {
- return createProcessorFactory().createProcessors( tab );
- }
-
- private ProcessorFactory createProcessorFactory() {
- return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
- }
-
- private DefinitionPane createDefinitionPane() {
- return new DefinitionPane();
- }
-
- private HTMLPreviewPane createHTMLPreviewPane() {
- return new HTMLPreviewPane();
- }
-
- private DefinitionSource createDefaultDefinitionSource() {
- return new YamlDefinitionSource( getDefinitionPath() );
- }
-
- private DefinitionSource createDefinitionSource( final Path path ) {
- try {
- return createDefinitionFactory().createDefinitionSource( path );
- } catch( final Exception ex ) {
- alert( ex );
- return createDefaultDefinitionSource();
- }
- }
-
- private TextField createFindTextField() {
- return new TextField();
- }
-
- private DefinitionFactory createDefinitionFactory() {
- return new DefinitionFactory();
- }
-
- private StatusBar createStatusBar() {
- return new StatusBar();
- }
-
- private Scene createScene() {
- final SplitPane splitPane = new SplitPane(
- getDefinitionPane(),
- getFileEditorPane(),
- getPreviewPane() );
-
- splitPane.setDividerPositions(
- getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
- getFloat( K_PANE_SPLIT_EDITOR, .60f ),
- getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
-
- getDefinitionPane().prefHeightProperty()
- .bind( splitPane.heightProperty() );
-
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1280, 800 );
- borderPane.setTop( createMenuBar() );
- borderPane.setBottom( getStatusBar() );
- borderPane.setCenter( splitPane );
-
- final VBox statusBar = new VBox();
- statusBar.setAlignment( Pos.BASELINE_CENTER );
- statusBar.getChildren().add( getLineNumberText() );
- getStatusBar().getRightItems().add( statusBar );
-
- // Force preview pane refresh on Windows.
- if( SystemUtils.IS_OS_WINDOWS ) {
- splitPane.getDividers().get( 1 ).positionProperty().addListener(
- ( l, oValue, nValue ) -> runLater(
- () -> getPreviewPane().getScrollPane().repaint()
- )
- );
- }
-
- return new Scene( borderPane );
- }
-
- private Text createLineNumberText() {
- return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
- }
-
- private Node createMenuBar() {
- final BooleanBinding activeFileEditorIsNull =
- getFileEditorPane().activeFileEditorProperty().isNull();
-
- // File actions
- final Action fileNewAction = new ActionBuilder()
- .setText( "Main.menu.file.new" )
- .setAccelerator( "Shortcut+N" )
- .setIcon( FILE_ALT )
- .setAction( e -> fileNew() )
- .build();
- final Action fileOpenAction = new ActionBuilder()
- .setText( "Main.menu.file.open" )
- .setAccelerator( "Shortcut+O" )
- .setIcon( FOLDER_OPEN_ALT )
- .setAction( e -> fileOpen() )
- .build();
- final Action fileCloseAction = new ActionBuilder()
- .setText( "Main.menu.file.close" )
- .setAccelerator( "Shortcut+W" )
- .setAction( e -> fileClose() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action fileCloseAllAction = new ActionBuilder()
- .setText( "Main.menu.file.close_all" )
- .setAction( e -> fileCloseAll() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action fileSaveAction = new ActionBuilder()
- .setText( "Main.menu.file.save" )
- .setAccelerator( "Shortcut+S" )
- .setIcon( FLOPPY_ALT )
- .setAction( e -> fileSave() )
- .setDisable( createActiveBooleanProperty(
- FileEditorTab::modifiedProperty ).not() )
- .build();
- final Action fileSaveAsAction = new ActionBuilder()
- .setText( "Main.menu.file.save_as" )
- .setAction( e -> fileSaveAs() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action fileSaveAllAction = new ActionBuilder()
- .setText( "Main.menu.file.save_all" )
- .setAccelerator( "Shortcut+Shift+S" )
- .setAction( e -> fileSaveAll() )
- .setDisable( Bindings.not(
- getFileEditorPane().anyFileEditorModifiedProperty() ) )
- .build();
- final Action fileExitAction = new ActionBuilder()
- .setText( "Main.menu.file.exit" )
- .setAction( e -> fileExit() )
- .build();
-
- // Edit actions
- final Action editCopyHtmlAction = new ActionBuilder()
- .setText( "Main.menu.edit.copy.html" )
- .setIcon( HTML5 )
- .setAction( e -> copyHtml() )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- final Action editUndoAction = new ActionBuilder()
- .setText( "Main.menu.edit.undo" )
- .setAccelerator( "Shortcut+Z" )
- .setIcon( UNDO )
- .setAction( e -> getActiveEditorPane().undo() )
- .setDisable( createActiveBooleanProperty(
- FileEditorTab::canUndoProperty ).not() )
- .build();
- final Action editRedoAction = new ActionBuilder()
- .setText( "Main.menu.edit.redo" )
- .setAccelerator( "Shortcut+Y" )
- .setIcon( REPEAT )
- .setAction( e -> getActiveEditorPane().redo() )
- .setDisable( createActiveBooleanProperty(
- FileEditorTab::canRedoProperty ).not() )
- .build();
-
- final Action editCutAction = new ActionBuilder()
- .setText( "Main.menu.edit.cut" )
- .setAccelerator( "Shortcut+X" )
- .setIcon( CUT )
- .setAction( e -> getActiveEditorPane().cut() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editCopyAction = new ActionBuilder()
- .setText( "Main.menu.edit.copy" )
- .setAccelerator( "Shortcut+C" )
- .setIcon( COPY )
- .setAction( e -> getActiveEditorPane().copy() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editPasteAction = new ActionBuilder()
- .setText( "Main.menu.edit.paste" )
- .setAccelerator( "Shortcut+V" )
- .setIcon( PASTE )
- .setAction( e -> getActiveEditorPane().paste() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editSelectAllAction = new ActionBuilder()
- .setText( "Main.menu.edit.selectAll" )
- .setAccelerator( "Shortcut+A" )
- .setAction( e -> getActiveEditorPane().selectAll() )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- final Action editFindAction = new ActionBuilder()
- .setText( "Main.menu.edit.find" )
- .setAccelerator( "Ctrl+F" )
- .setIcon( SEARCH )
- .setAction( e -> editFind() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editFindNextAction = new ActionBuilder()
- .setText( "Main.menu.edit.find.next" )
- .setAccelerator( "F3" )
- .setIcon( null )
- .setAction( e -> editFindNext() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editPreferencesAction = new ActionBuilder()
- .setText( "Main.menu.edit.preferences" )
- .setAccelerator( "Ctrl+Alt+S" )
- .setAction( e -> editPreferences() )
- .build();
-
- // Format actions
- final Action formatBoldAction = new ActionBuilder()
- .setText( "Main.menu.format.bold" )
- .setAccelerator( "Shortcut+B" )
- .setIcon( BOLD )
- .setAction( e -> insertMarkdown( "**", "**" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action formatItalicAction = new ActionBuilder()
- .setText( "Main.menu.format.italic" )
- .setAccelerator( "Shortcut+I" )
- .setIcon( ITALIC )
- .setAction( e -> insertMarkdown( "*", "*" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action formatSuperscriptAction = new ActionBuilder()
- .setText( "Main.menu.format.superscript" )
- .setAccelerator( "Shortcut+[" )
- .setIcon( SUPERSCRIPT )
- .setAction( e -> insertMarkdown( "^", "^" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action formatSubscriptAction = new ActionBuilder()
- .setText( "Main.menu.format.subscript" )
- .setAccelerator( "Shortcut+]" )
- .setIcon( SUBSCRIPT )
- .setAction( e -> insertMarkdown( "~", "~" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action formatStrikethroughAction = new ActionBuilder()
- .setText( "Main.menu.format.strikethrough" )
- .setAccelerator( "Shortcut+T" )
- .setIcon( STRIKETHROUGH )
- .setAction( e -> insertMarkdown( "~~", "~~" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- // Insert actions
- final Action insertBlockquoteAction = new ActionBuilder()
- .setText( "Main.menu.insert.blockquote" )
- .setAccelerator( "Ctrl+Q" )
- .setIcon( QUOTE_LEFT )
- .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertCodeAction = new ActionBuilder()
- .setText( "Main.menu.insert.code" )
- .setAccelerator( "Shortcut+K" )
- .setIcon( CODE )
- .setAction( e -> insertMarkdown( "`", "`" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertFencedCodeBlockAction = new ActionBuilder()
- .setText( "Main.menu.insert.fenced_code_block" )
- .setAccelerator( "Shortcut+Shift+K" )
- .setIcon( FILE_CODE_ALT )
- .setAction( e -> insertMarkdown(
- "\n\n```\n",
- "\n```\n\n",
- get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertLinkAction = new ActionBuilder()
- .setText( "Main.menu.insert.link" )
- .setAccelerator( "Shortcut+L" )
- .setIcon( LINK )
- .setAction( e -> getActiveEditorPane().insertLink() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertImageAction = new ActionBuilder()
- .setText( "Main.menu.insert.image" )
- .setAccelerator( "Shortcut+G" )
- .setIcon( PICTURE_ALT )
- .setAction( e -> getActiveEditorPane().insertImage() )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- // Number of heading actions (H1 ... H3)
- final int HEADINGS = 3;
- final Action[] headings = new Action[ HEADINGS ];
-
- for( int i = 1; i <= HEADINGS; i++ ) {
- final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
- final String markup = String.format( "%n%n%s ", hashes );
- final String text = "Main.menu.insert.heading." + i;
- final String accelerator = "Shortcut+" + i;
- final String prompt = text + ".prompt";
-
- headings[ i - 1 ] = new ActionBuilder()
- .setText( text )
- .setAccelerator( accelerator )
- .setIcon( HEADER )
- .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- }
-
- final Action insertUnorderedListAction = new ActionBuilder()
- .setText( "Main.menu.insert.unordered_list" )
- .setAccelerator( "Shortcut+U" )
- .setIcon( LIST_UL )
- .setAction( e -> insertMarkdown( "\n\n* ", "" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertOrderedListAction = new ActionBuilder()
- .setText( "Main.menu.insert.ordered_list" )
- .setAccelerator( "Shortcut+Shift+O" )
- .setIcon( LIST_OL )
- .setAction( e -> insertMarkdown(
- "\n\n1. ", "" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertHorizontalRuleAction = new ActionBuilder()
- .setText( "Main.menu.insert.horizontal_rule" )
- .setAccelerator( "Shortcut+H" )
- .setAction( e -> insertMarkdown(
- "\n\n---\n\n", "" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- // Definition actions
- final Action definitionCreateAction = new ActionBuilder()
- .setText( "Main.menu.definition.create" )
- .setIcon( TREE )
- .setAction( e -> getDefinitionPane().addItem() )
- .build();
- final Action definitionInsertAction = new ActionBuilder()
- .setText( "Main.menu.definition.insert" )
- .setAccelerator( "Ctrl+Space" )
- .setIcon( STAR )
- .setAction( e -> definitionInsert() )
- .build();
-
- // Help actions
- final Action helpAboutAction = new ActionBuilder()
- .setText( "Main.menu.help.about" )
- .setAction( e -> helpAbout() )
- .build();
-
- //---- MenuBar ----
-
- // File Menu
- final var fileMenu = ActionUtils.createMenu(
- get( "Main.menu.file" ),
- fileNewAction,
- fileOpenAction,
- null,
- fileCloseAction,
- fileCloseAllAction,
- null,
- fileSaveAction,
- fileSaveAsAction,
- fileSaveAllAction,
- null,
- fileExitAction );
-
- // Edit Menu
- final var editMenu = ActionUtils.createMenu(
- get( "Main.menu.edit" ),
- editCopyHtmlAction,
- null,
- editUndoAction,
- editRedoAction,
- null,
- editCutAction,
- editCopyAction,
- editPasteAction,
- editSelectAllAction,
- null,
- editFindAction,
- editFindNextAction,
- null,
- editPreferencesAction );
-
- // Format Menu
- final var formatMenu = ActionUtils.createMenu(
- get( "Main.menu.format" ),
- formatBoldAction,
- formatItalicAction,
- formatSuperscriptAction,
- formatSubscriptAction,
- formatStrikethroughAction
- );
-
- // Insert Menu
- final var insertMenu = ActionUtils.createMenu(
- get( "Main.menu.insert" ),
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headings[ 0 ],
- headings[ 1 ],
- headings[ 2 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction,
- insertHorizontalRuleAction
- );
-
- // Definition Menu
- final var definitionMenu = ActionUtils.createMenu(
- get( "Main.menu.definition" ),
- definitionCreateAction,
- definitionInsertAction );
-
- // Help Menu
- final var helpMenu = ActionUtils.createMenu(
- get( "Main.menu.help" ),
- helpAboutAction );
-
- //---- MenuBar ----
- final var menuBar = new MenuBar(
- fileMenu,
- editMenu,
- formatMenu,
- insertMenu,
- definitionMenu,
- helpMenu );
-
- //---- ToolBar ----
- final var toolBar = ActionUtils.createToolBar(
- fileNewAction,
- fileOpenAction,
- fileSaveAction,
- null,
- editUndoAction,
- editRedoAction,
- editCutAction,
- editCopyAction,
- editPasteAction,
- null,
- formatBoldAction,
- formatItalicAction,
- formatSuperscriptAction,
- formatSubscriptAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headings[ 0 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction );
-
- return new VBox( menuBar, toolBar );
- }
-
- /**
- * Performs the autoinsert function on the active file editor.
- */
- private void definitionInsert() {
- getDefinitionNameInjector().autoinsert();
- }
-
- /**
- * Creates a boolean property that is bound to another boolean value of the
- * active editor.
- */
- private BooleanProperty createActiveBooleanProperty(
- final Function<FileEditorTab, ObservableBooleanValue> func ) {
-
- final BooleanProperty b = new SimpleBooleanProperty();
- final FileEditorTab tab = getActiveFileEditorTab();
-
- if( tab != null ) {
- b.bind( func.apply( tab ) );
- }
-
- getFileEditorPane().activeFileEditorProperty().addListener(
- ( observable, oldFileEditor, newFileEditor ) -> {
- b.unbind();
-
- if( newFileEditor == null ) {
- b.set( false );
- }
- else {
- b.bind( func.apply( newFileEditor ) );
- }
- }
- );
-
- return b;
- }
-
- //---- Convenience accessors ----------------------------------------------
-
- private Preferences getPreferences() {
- return sOptions.getState();
- }
-
- private int getCurrentParagraphIndex() {
- return getActiveEditorPane().getCurrentParagraphIndex();
- }
-
- private float getFloat( final String key, final float defaultValue ) {
- return getPreferences().getFloat( key, defaultValue );
- }
-
- public Window getWindow() {
- return getScene().getWindow();
- }
-
- private MarkdownEditorPane getActiveEditorPane() {
- return getActiveFileEditorTab().getEditorPane();
- }
-
- private FileEditorTab getActiveFileEditorTab() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- //---- Member accessors ---------------------------------------------------
-
- protected Scene getScene() {
- return mScene;
- }
-
- private SpellChecker getSpellChecker() {
- return mSpellChecker;
- }
-
- private Map<FileEditorTab, Processor<String>> getProcessors() {
- return mProcessors;
- }
-
- private FileEditorTabPane getFileEditorPane() {
- return mFileEditorPane;
- }
-
- private HTMLPreviewPane getPreviewPane() {
- return mPreviewPane;
- }
-
- private void setDefinitionSource(
- final DefinitionSource definitionSource ) {
- assert definitionSource != null;
- mDefinitionSource = definitionSource;
- }
-
- private DefinitionSource getDefinitionSource() {
- return mDefinitionSource;
- }
-
- private DefinitionPane getDefinitionPane() {
- return mDefinitionPane;
- }
-
- private Text getLineNumberText() {
- return mLineNumberText;
- }
-
- private StatusBar getStatusBar() {
- return mStatusBar;
- }
-
- private TextField getFindTextField() {
- return mFindTextField;
- }
-
- private DefinitionNameInjector getDefinitionNameInjector() {
- return mDefinitionNameInjector;
- }
-
- /**
- * Returns the variable map of interpolated definitions.
- *
- * @return A map to help dereference variables.
- */
- private Map<String, String> getResolvedMap() {
- return mResolvedMap;
- }
-
- //---- Persistence accessors ----------------------------------------------
-
- private UserPreferences getUserPreferences() {
- return UserPreferences.getInstance();
- }
-
- private Path getDefinitionPath() {
- return getUserPreferences().getDefinitionPath();
- }
-
- //---- Spelling -----------------------------------------------------------
-
- /**
- * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}.
- * This is called to spell check the document, rather than a single paragraph.
- *
- * @param text The full document text.
- */
- private void spellcheck(
- final StyleClassedTextArea editor, final String text ) {
- spellcheck( editor, text, -1 );
- }
-
- /**
- * Spellchecks a subset of the entire document.
- *
- * @param text Look up words for this text in the lexicon.
- * @param paraId Set to -1 to apply resulting style spans to the entire
- * text.
- */
- private void spellcheck(
- final StyleClassedTextArea editor, final String text, final int paraId ) {
- final var builder = new StyleSpansBuilder<Collection<String>>();
- final var runningIndex = new AtomicInteger( 0 );
- final var checker = getSpellChecker();
-
- // The text nodes must be relayed through a contextual "visitor" that
- // can return text in chunks with correlative offsets into the string.
- // This allows Markdown, R Markdown, XML, and R XML documents to return
- // sets of words to check.
-
- final var node = mParser.parse( text );
- final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
- // Treat hyphenated compound words as individual words.
- final var check = visited.replace( '-', ' ' );
-
- checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
- prevIndex += bIndex;
- currIndex += bIndex;
-
- // Clear styling between lexiconically absent words.
- builder.add( emptyList(), prevIndex - runningIndex.get() );
- builder.add( singleton( "spelling" ), currIndex - prevIndex );
- runningIndex.set( currIndex );
- } );
- } );
-
- visitor.visit( node );
-
- // If the running index was set, at least one word triggered the listener.
- if( runningIndex.get() > 0 ) {
- // Clear styling after the last lexiconically absent word.
- builder.add( emptyList(), text.length() - runningIndex.get() );
-
- final var spans = builder.create();
-
- if( paraId >= 0 ) {
- editor.setStyleSpans( paraId, 0, spans );
- }
- else {
- editor.setStyleSpans( 0, spans );
- }
- }
- }
-
- @SuppressWarnings("SameParameterValue")
- private Collection<String> readLexicon( final String filename )
- throws Exception {
- final var path = "/" + LEXICONS_DIRECTORY + "/" + filename;
-
- try( final var resource = getClass().getResourceAsStream( path ) ) {
- if( resource == null ) {
- throw new FileNotFoundException( path );
- }
-
- try( final var isr = new InputStreamReader( resource, UTF_8 );
- final var reader = new BufferedReader( isr ) ) {
- return reader.lines().collect( Collectors.toList() );
- }
- }
- }
-
- // TODO: Replace using Markdown processor instantiated for Markdown files.
- // FIXME: https://github.com/DaveJarvis/scrivenvar/issues/59
- private final Parser mParser = Parser.builder().build();
-
- // TODO: Replace with generic interface; provide Markdown/XML implementations.
- // FIXME: https://github.com/DaveJarvis/scrivenvar/issues/59
- private static final class TextVisitor {
- private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
- com.vladsch.flexmark.ast.Text.class, this::visit )
- );
-
- private final SpellCheckListener mConsumer;
-
- public TextVisitor( final SpellCheckListener consumer ) {
- mConsumer = consumer;
- }
-
- private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
- if( node instanceof com.vladsch.flexmark.ast.Text ) {
- mConsumer.accept( node.getChars().toString(),
- node.getStartOffset(),
- node.getEndOffset() );
- }
-
- mVisitor.visitChildren( node );
- }
- }
-}
src/main/java/com/scrivenvar/Messages.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import java.text.MessageFormat;
-import java.util.ResourceBundle;
-import java.util.Stack;
-
-import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
-import static java.util.ResourceBundle.getBundle;
-
-/**
- * Recursively resolves message properties. Property values can refer to other
- * properties using a <code>${var}</code> syntax.
- */
-public class Messages {
-
- private static final ResourceBundle RESOURCE_BUNDLE =
- getBundle( APP_BUNDLE_NAME );
-
- private Messages() {
- }
-
- /**
- * Return the value of a resource bundle value after having resolved any
- * references to other bundle variables.
- *
- * @param props The bundle containing resolvable properties.
- * @param s The value for a key to resolve.
- * @return The value of the key with all references recursively dereferenced.
- */
- @SuppressWarnings("SameParameterValue")
- private static String resolve( final ResourceBundle props, final String s ) {
- final int len = s.length();
- final Stack<StringBuilder> stack = new Stack<>();
-
- StringBuilder sb = new StringBuilder( 256 );
- boolean open = false;
-
- for( int i = 0; i < len; i++ ) {
- final char c = s.charAt( i );
-
- switch( c ) {
- case '$': {
- if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
- stack.push( sb );
- sb = new StringBuilder( 256 );
- i++;
- open = true;
- }
-
- break;
- }
-
- case '}': {
- if( open ) {
- open = false;
- final String name = sb.toString();
-
- sb = stack.pop();
- sb.append( props.getString( name ) );
- break;
- }
- }
-
- default: {
- sb.append( c );
- break;
- }
- }
- }
-
- if( open ) {
- throw new IllegalArgumentException( "missing '}'" );
- }
-
- return sb.toString();
- }
-
- /**
- * Returns the value for a key from the message bundle.
- *
- * @param key Retrieve the value for this key.
- * @return The value for the key.
- */
- public static String get( final String key ) {
- try {
- return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
- } catch( final Exception ex ) {
- return key;
- }
- }
-
- public static String getLiteral( final String key ) {
- return RESOURCE_BUNDLE.getString( key );
- }
-
- public static String get( final String key, final boolean interpolate ) {
- return interpolate ? get( key ) : getLiteral( key );
- }
-
- /**
- * Returns the value for a key from the message bundle with the arguments
- * replacing <code>{#}</code> place holders.
- *
- * @param key Retrieve the value for this key.
- * @param args The values to substitute for place holders.
- * @return The value for the key.
- */
- public static String get( final String key, final Object... args ) {
- return MessageFormat.format( get( key ), args );
- }
-}
src/main/java/com/scrivenvar/ScrollEventHandler.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.scene.Node;
-import javafx.scene.control.ScrollBar;
-import javafx.scene.control.skin.ScrollBarSkin;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.input.ScrollEvent;
-import javafx.scene.layout.StackPane;
-import org.fxmisc.flowless.VirtualizedScrollPane;
-import org.fxmisc.richtext.StyleClassedTextArea;
-
-import javax.swing.*;
-
-import static javafx.geometry.Orientation.VERTICAL;
-
-/**
- * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to
- * an instance of {@link JScrollBar}.
- * <p>
- * Called to synchronize the scrolling areas for either scrolling with the
- * mouse or scrolling using the scrollbar's thumb. Both are required to avoid
- * scrolling on the estimatedScrollYProperty that occurs when text events
- * fire. Scrolling performed for text events are handled separately to ensure
- * the preview panel scrolls to the same position in the Markdown editor,
- * taking into account things like images, tables, and other potentially
- * long vertical presentation items.
- * </p>
- */
-public final class ScrollEventHandler implements EventHandler<Event> {
-
- private final class MouseHandler implements EventHandler<MouseEvent> {
- private final EventHandler<? super MouseEvent> mOldHandler;
-
- /**
- * Constructs a new handler for mouse scrolling events.
- *
- * @param oldHandler Receives the event after scrolling takes place.
- */
- private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) {
- mOldHandler = oldHandler;
- }
-
- @Override
- public void handle( final MouseEvent event ) {
- ScrollEventHandler.this.handle( event );
- mOldHandler.handle( event );
- }
- }
-
- private final class ScrollHandler implements EventHandler<ScrollEvent> {
- @Override
- public void handle( final ScrollEvent event ) {
- ScrollEventHandler.this.handle( event );
- }
- }
-
- private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane;
- private final JScrollBar mPreviewScrollBar;
- private final BooleanProperty mEnabled = new SimpleBooleanProperty();
-
- /**
- * @param editorScrollPane Scroll event source (human movement).
- * @param previewScrollBar Scroll event destination (corresponding movement).
- */
- public ScrollEventHandler(
- final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane,
- final JScrollBar previewScrollBar ) {
- mEditorScrollPane = editorScrollPane;
- mPreviewScrollBar = previewScrollBar;
-
- mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
-
- final var thumb = getVerticalScrollBarThumb( mEditorScrollPane );
- thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) );
- }
-
- /**
- * Gets a property intended to be bound to selected property of the tab being
- * scrolled. This is required because there's only one preview pane but
- * multiple editor panes. Each editor pane maintains its own scroll position.
- *
- * @return A {@link BooleanProperty} representing whether the scroll
- * events for this tab are to be executed.
- */
- public BooleanProperty enabledProperty() {
- return mEnabled;
- }
-
- /**
- * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm
- * is based on Karl Tauber's ratio calculation.
- *
- * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent}
- */
- @Override
- public void handle( final Event event ) {
- if( isEnabled() ) {
- final var eScrollPane = getEditorScrollPane();
- final int eScrollY =
- eScrollPane.estimatedScrollYProperty().getValue().intValue();
- final int eHeight = (int)
- (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
- - eScrollPane.getHeight());
- final double eRatio = eHeight > 0
- ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
-
- final var pScrollBar = getPreviewScrollBar();
- final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
- final var pScrollY = (int) (pHeight * eRatio);
-
- pScrollBar.setValue( pScrollY );
- pScrollBar.getParent().repaint();
- }
- }
-
- private StackPane getVerticalScrollBarThumb(
- final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
- final ScrollBar scrollBar = getVerticalScrollBar( pane );
- final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get());
-
- for( final Node node : skin.getChildren() ) {
- // Brittle, but what can you do?
- if( node.getStyleClass().contains( "thumb" ) ) {
- return (StackPane) node;
- }
- }
-
- throw new IllegalArgumentException( "No scroll bar skin found." );
- }
-
- private ScrollBar getVerticalScrollBar(
- final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
-
- for( final Node node : pane.getChildrenUnmodifiable() ) {
- if( node instanceof ScrollBar ) {
- final ScrollBar scrollBar = (ScrollBar) node;
-
- if( scrollBar.getOrientation() == VERTICAL ) {
- return scrollBar;
- }
- }
- }
-
- throw new IllegalArgumentException( "No vertical scroll pane found." );
- }
-
- private boolean isEnabled() {
- // TODO: As a minor optimization, when this is set to false, it could remove
- // the MouseHandler and ScrollHandler so that events only dispatch to one
- // object (instead of one per editor tab).
- return mEnabled.get();
- }
-
- private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() {
- return mEditorScrollPane;
- }
-
- private JScrollBar getPreviewScrollBar() {
- return mPreviewScrollBar;
- }
-}
src/main/java/com/scrivenvar/Services.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.ServiceLoader;
-
-/**
- * Responsible for loading services. The services are treated as singleton
- * instances.
- */
-public class Services {
-
- @SuppressWarnings("rawtypes")
- private static final Map<Class, Object> SINGLETONS = new HashMap<>();
-
- /**
- * Loads a service based on its interface definition. This will return an
- * existing instance if the class has already been instantiated.
- *
- * @param <T> The service to load.
- * @param api The interface definition for the service.
- * @return A class that implements the interface.
- */
- @SuppressWarnings("unchecked")
- public static <T> T load( final Class<T> api ) {
- final T o = (T) get( api );
-
- return o == null ? newInstance( api ) : o;
- }
-
- private static <T> T newInstance( final Class<T> api ) {
- final ServiceLoader<T> services = ServiceLoader.load( api );
-
- for( final T service : services ) {
- if( service != null ) {
- // Re-use the same instance the next time the class is loaded.
- put( api, service );
- return service;
- }
- }
-
- throw new RuntimeException( "No implementation for: " + api );
- }
-
- @SuppressWarnings("rawtypes")
- private static void put( final Class key, Object value ) {
- SINGLETONS.put( key, value );
- }
-
- @SuppressWarnings("rawtypes")
- private static Object get( final Class api ) {
- return SINGLETONS.get( api );
- }
-}
src/main/java/com/scrivenvar/StatusBarNotifier.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar;
-
-import com.scrivenvar.service.events.Notifier;
-import org.controlsfx.control.StatusBar;
-
-import static com.scrivenvar.Constants.STATUS_BAR_OK;
-import static com.scrivenvar.Messages.get;
-import static javafx.application.Platform.runLater;
-
-/**
- * Responsible for passing notifications about exceptions (or other error
- * messages) through the application. Once the Event Bus is implemented, this
- * class can go away.
- */
-public class StatusBarNotifier {
- private static final String OK = get( STATUS_BAR_OK, "OK" );
-
- private static final Notifier sNotifier = Services.load( Notifier.class );
- private static StatusBar sStatusBar;
-
- public static void setStatusBar( final StatusBar statusBar ) {
- sStatusBar = statusBar;
- }
-
- /**
- * Resets the status bar to a default message.
- */
- public static void clearAlert() {
- // Don't burden the repaint thread if there's no status bar change.
- if( !OK.equals( sStatusBar.getText() ) ) {
- update( OK );
- }
- }
-
- /**
- * Updates the status bar with a custom message.
- *
- * @param key The resource bundle key associated with a message (typically
- * to inform the user about an error).
- */
- public static void alert( final String key ) {
- update( get( key ) );
- }
-
- /**
- * Updates the status bar with a custom message.
- *
- * @param key The property key having a value to populate with arguments.
- * @param args The placeholder values to substitute into the key's value.
- */
- public static void alert( final String key, final Object... args ) {
- update( get( key, args ) );
- }
-
- /**
- * Called when an exception occurs that warrants the user's attention.
- *
- * @param t The exception with a message that the user should know about.
- */
- public static void alert( final Throwable t ) {
- update( t.getMessage() );
- }
-
- /**
- * Updates the status bar to show the first line of the given message.
- *
- * @param message The message to show in the status bar.
- */
- private static void update( final String message ) {
- runLater(
- () -> {
- final var s = message == null ? "" : message;
- final var i = s.indexOf( '\n' );
- sStatusBar.setText( s.substring( 0, i > 0 ? i : s.length() ) );
- }
- );
- }
-
- /**
- * Returns the global {@link Notifier} instance that can be used for opening
- * pop-up alert messages.
- *
- * @return The pop-up {@link Notifier} dispatcher.
- */
- public static Notifier getNotifier() {
- return sNotifier;
- }
-}
src/main/java/com/scrivenvar/adapters/DocumentAdapter.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.adapters;
-
-import org.xhtmlrenderer.event.DocumentListener;
-
-import static com.scrivenvar.StatusBarNotifier.alert;
-
-/**
- * Allows subclasses to implement specific events.
- */
-public class DocumentAdapter implements DocumentListener {
- @Override
- public void documentStarted() {
- }
-
- @Override
- public void documentLoaded() {
- }
-
- @Override
- public void onLayoutException( final Throwable t ) {
- alert( t );
- }
-
- @Override
- public void onRenderException( final Throwable t ) {
- alert( t );
- }
-}
src/main/java/com/scrivenvar/adapters/ReplacedElementAdapter.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.adapters;
-
-import org.w3c.dom.Element;
-import org.xhtmlrenderer.extend.ReplacedElementFactory;
-import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
-
-public abstract class ReplacedElementAdapter implements ReplacedElementFactory {
- @Override
- public void reset() {
- }
-
- @Override
- public void remove( final Element e ) {
- }
-
- @Override
- public void setFormSubmissionListener(
- final FormSubmissionListener listener ) {
- }
-}
src/main/java/com/scrivenvar/controls/BrowseFileButton.java
-/*
- * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.scrivenvar.controls;
-
-import com.scrivenvar.Messages;
-import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
-import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.event.ActionEvent;
-import javafx.scene.control.Button;
-import javafx.scene.control.Tooltip;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyEvent;
-import javafx.stage.FileChooser;
-import javafx.stage.FileChooser.ExtensionFilter;
-
-import java.io.File;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Button that opens a file chooser to select a local file for a URL.
- */
-public class BrowseFileButton extends Button {
- private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
-
- public BrowseFileButton() {
- setGraphic(
- FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT )
- );
- setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
- setOnAction( this::browse );
-
- disableProperty().bind( basePath.isNull() );
-
- // workaround for a JavaFX bug:
- // avoid closing the dialog that contains this control when the user
- // closes the FileChooser or DirectoryChooser using the ESC key
- addEventHandler( KeyEvent.KEY_RELEASED, e -> {
- if( e.getCode() == KeyCode.ESCAPE ) {
- e.consume();
- }
- } );
- }
-
- public void addExtensionFilter( ExtensionFilter extensionFilter ) {
- extensionFilters.add( extensionFilter );
- }
-
- // 'basePath' property
- private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
-
- public Path getBasePath() {
- return basePath.get();
- }
-
- public void setBasePath( Path basePath ) {
- this.basePath.set( basePath );
- }
-
- // 'url' property
- private final ObjectProperty<String> url = new SimpleObjectProperty<>();
-
- public ObjectProperty<String> urlProperty() {
- return url;
- }
-
- protected void browse( ActionEvent e ) {
- FileChooser fileChooser = new FileChooser();
- fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
- fileChooser.getExtensionFilters().addAll( extensionFilters );
- fileChooser.getExtensionFilters()
- .add( new ExtensionFilter( Messages.get(
- "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
- fileChooser.setInitialDirectory( getInitialDirectory() );
- File result = fileChooser.showOpenDialog( getScene().getWindow() );
- if( result != null ) {
- updateUrl( result );
- }
- }
-
- protected File getInitialDirectory() {
- //TODO build initial directory based on current value of 'url' property
- return getBasePath().toFile();
- }
-
- protected void updateUrl( File file ) {
- String newUrl;
- try {
- newUrl = getBasePath().relativize( file.toPath() ).toString();
- } catch( IllegalArgumentException ex ) {
- newUrl = file.toString();
- }
- url.set( newUrl.replace( '\\', '/' ) );
- }
-}
src/main/java/com/scrivenvar/controls/EscapeTextField.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package com.scrivenvar.controls;
-
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-import javafx.scene.control.TextField;
-import javafx.util.StringConverter;
-
-/**
- * Responsible for escaping/unescaping characters for markdown.
- */
-public class EscapeTextField extends TextField {
-
- public EscapeTextField() {
- escapedText.bindBidirectional(
- textProperty(),
- new StringConverter<>() {
- @Override
- public String toString( String object ) {
- return escape( object );
- }
-
- @Override
- public String fromString( String string ) {
- return unescape( string );
- }
- }
- );
- escapeCharacters.addListener(
- e -> escapedText.set( escape( textProperty().get() ) )
- );
- }
-
- // 'escapedText' property
- private final StringProperty escapedText = new SimpleStringProperty();
-
- public StringProperty escapedTextProperty() {
- return escapedText;
- }
-
- // 'escapeCharacters' property
- private final StringProperty escapeCharacters = new SimpleStringProperty();
-
- public String getEscapeCharacters() {
- return escapeCharacters.get();
- }
-
- public void setEscapeCharacters( String escapeCharacters ) {
- this.escapeCharacters.set( escapeCharacters );
- }
-
- private String escape( final String s ) {
- final String escapeChars = getEscapeCharacters();
-
- return isEmpty( escapeChars ) ? s :
- s.replaceAll( "([" + escapeChars.replaceAll(
- "(.)",
- "\\\\$1" ) + "])", "\\\\$1" );
- }
-
- private String unescape( final String s ) {
- final String escapeChars = getEscapeCharacters();
-
- return isEmpty( escapeChars ) ? s :
- s.replaceAll( "\\\\([" + escapeChars
- .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
- }
-
- private static boolean isEmpty( final String s ) {
- return s == null || s.isEmpty();
- }
-}
src/main/java/com/scrivenvar/definition/DefinitionFactory.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import com.scrivenvar.AbstractFileFactory;
-import com.scrivenvar.FileType;
-import com.scrivenvar.definition.yaml.YamlDefinitionSource;
-
-import java.nio.file.Path;
-
-import static com.scrivenvar.Constants.GLOB_PREFIX_DEFINITION;
-import static com.scrivenvar.FileType.YAML;
-import static com.scrivenvar.util.ProtocolResolver.getProtocol;
-
-/**
- * Responsible for creating objects that can read and write definition data
- * sources. The data source could be YAML, TOML, JSON, flat files, or from a
- * database.
- */
-public class DefinitionFactory extends AbstractFileFactory {
-
- /**
- * Default (empty) constructor.
- */
- public DefinitionFactory() {
- }
-
- /**
- * Creates a definition source capable of reading definitions from the given
- * path.
- *
- * @param path Path to a resource containing definitions.
- * @return The definition source appropriate for the given path.
- */
- public DefinitionSource createDefinitionSource( final Path path ) {
- assert path != null;
-
- final var protocol = getProtocol( path.toString() );
- DefinitionSource result = null;
-
- if( protocol.isFile() ) {
- final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION );
- result = createFileDefinitionSource( filetype, path );
- }
- else {
- unknownFileType( protocol, path.toString() );
- }
-
- return result;
- }
-
- /**
- * Creates a definition source based on the file type.
- *
- * @param filetype Property key name suffix from settings.properties file.
- * @param path Path to the file that corresponds to the extension.
- * @return A DefinitionSource capable of parsing the data stored at the path.
- */
- private DefinitionSource createFileDefinitionSource(
- final FileType filetype, final Path path ) {
- assert filetype != null;
- assert path != null;
-
- if( filetype == YAML ) {
- return new YamlDefinitionSource( path );
- }
-
- throw new IllegalArgumentException( filetype.toString() );
- }
-}
src/main/java/com/scrivenvar/definition/DefinitionPane.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
-import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-import javafx.collections.ObservableList;
-import javafx.event.ActionEvent;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.control.*;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.HBox;
-import javafx.util.StringConverter;
-
-import java.util.*;
-
-import static com.scrivenvar.Messages.get;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
-import static javafx.geometry.Pos.CENTER;
-import static javafx.scene.input.KeyEvent.KEY_PRESSED;
-
-/**
- * Provides the user interface that holds a {@link TreeView}, which
- * allows users to interact with key/value pairs loaded from the
- * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
- */
-public final class DefinitionPane extends BorderPane {
-
- /**
- * Contains a view of the definitions.
- */
- private final TreeView<String> mTreeView = new TreeView<>();
-
- /**
- * Handlers for key press events.
- */
- private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
- = new HashSet<>();
-
- /**
- * Definition file name shown in the title of the pane.
- */
- private final StringProperty mFilename = new SimpleStringProperty();
-
- private final TitledPane mTitledPane = new TitledPane();
-
- /**
- * Constructs a definition pane with a given tree view root.
- */
- public DefinitionPane() {
- final var treeView = getTreeView();
- treeView.setEditable( true );
- treeView.setCellFactory( cell -> createTreeCell() );
- treeView.setContextMenu( createContextMenu() );
- treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
- treeView.setShowRoot( false );
- getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
-
- final var bCreate = createButton(
- "create", TREE, e -> addItem() );
- final var bRename = createButton(
- "rename", EDIT, e -> editSelectedItem() );
- final var bDelete = createButton(
- "delete", TRASH, e -> deleteSelectedItems() );
-
- final var buttonBar = new HBox();
- buttonBar.getChildren().addAll( bCreate, bRename, bDelete );
- buttonBar.setAlignment( CENTER );
- buttonBar.setSpacing( 10 );
-
- final var titledPane = getTitledPane();
- titledPane.textProperty().bind( mFilename );
- titledPane.setContent( treeView );
- titledPane.setCollapsible( false );
- titledPane.setPadding( new Insets( 0, 0, 0, 0 ) );
-
- setTop( buttonBar );
- setCenter( titledPane );
- setAlignment( buttonBar, Pos.TOP_CENTER );
- setAlignment( titledPane, Pos.TOP_CENTER );
-
- titledPane.prefHeightProperty().bind( this.heightProperty() );
- }
-
- public void setTooltip( final Tooltip tooltip ) {
- getTitledPane().setTooltip( tooltip );
- }
-
- private TitledPane getTitledPane() {
- return mTitledPane;
- }
-
- private Button createButton(
- final String msgKey,
- final FontAwesomeIcon icon,
- final EventHandler<ActionEvent> eventHandler ) {
- final var keyPrefix = "Pane.definition.button." + msgKey;
- final var button = new Button( get( keyPrefix + ".label" ) );
- button.setOnAction( eventHandler );
-
- button.setGraphic(
- FontAwesomeIconFactory.get().createIcon( icon )
- );
- button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
-
- return button;
- }
-
- /**
- * Changes the root of the {@link TreeView} to the root of the
- * {@link TreeView} from the {@link DefinitionSource}.
- *
- * @param definitionSource Container for the hierarchy of key/value pairs
- * to replace the existing hierarchy.
- */
- public void update( final DefinitionSource definitionSource ) {
- assert definitionSource != null;
-
- final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
- final TreeItem<String> root = treeAdapter.adapt(
- get( "Pane.definition.node.root.title" )
- );
-
- getTreeView().setRoot( root );
- }
-
- public Map<String, String> toMap() {
- return TreeItemAdapter.toMap( getTreeView().getRoot() );
- }
-
- /**
- * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
- * is modified. The modifications include: item value changes, item additions,
- * and item removals.
- * <p>
- * Safe to call multiple times; if a handler is already registered, the
- * old handler is used.
- * </p>
- *
- * @param handler The handler to call whenever any {@link TreeItem} changes.
- */
- public void addTreeChangeHandler(
- final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
- final TreeItem<String> root = getTreeView().getRoot();
- root.addEventHandler( TreeItem.valueChangedEvent(), handler );
- root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
- }
-
- public void addKeyEventHandler(
- final EventHandler<? super KeyEvent> handler ) {
- getKeyEventHandlers().add( handler );
- }
-
- /**
- * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
- * well-formed for export. A tree is considered well-formed if the following
- * conditions are met:
- *
- * <ul>
- * <li>The root node contains at least one child node having a leaf.</li>
- * <li>There are no leaf nodes with sibling leaf nodes.</li>
- * </ul>
- *
- * @return {@code null} if the document is well-formed, otherwise the
- * problematic child {@link TreeItem}.
- */
- public TreeItem<String> isTreeWellFormed() {
- final var root = getTreeView().getRoot();
-
- for( final var child : root.getChildren() ) {
- final var problemChild = isWellFormed( child );
-
- if( child.isLeaf() || problemChild != null ) {
- return problemChild;
- }
- }
-
- return null;
- }
-
- /**
- * Determines whether the document is well-formed by ensuring that
- * child branches do not contain multiple leaves.
- *
- * @param item The sub-tree to check for well-formedness.
- * @return {@code null} when the tree is well-formed, otherwise the
- * problematic {@link TreeItem}.
- */
- private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
- int childLeafs = 0;
- int childBranches = 0;
-
- for( final TreeItem<String> child : item.getChildren() ) {
- if( child.isLeaf() ) {
- childLeafs++;
- }
- else {
- childBranches++;
- }
-
- final var problemChild = isWellFormed( child );
-
- if( problemChild != null ) {
- return problemChild;
- }
- }
-
- return ((childBranches > 0 && childLeafs == 0) ||
- (childBranches == 0 && childLeafs <= 1)) ? null : item;
- }
-
- /**
- * Delegates to {@link DefinitionTreeItem#findLeafExact(String)}.
- *
- * @param text The value to find, never {@code null}.
- * @return The leaf that contains the given value, or {@code null} if
- * not found.
- */
- public DefinitionTreeItem<String> findLeafExact( final String text ) {
- return getTreeRoot().findLeafExact( text );
- }
-
- /**
- * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
- *
- * @param text The value to find, never {@code null}.
- * @return The leaf that contains the given value, or {@code null} if
- * not found.
- */
- public DefinitionTreeItem<String> findLeafContains( final String text ) {
- return getTreeRoot().findLeafContains( text );
- }
-
- /**
- * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}.
- *
- * @param text The value to find, never {@code null}.
- * @return The leaf that contains the given value, or {@code null} if
- * not found.
- */
- public DefinitionTreeItem<String> findLeafContainsNoCase(
- final String text ) {
- return getTreeRoot().findLeafContainsNoCase( text );
- }
-
- /**
- * Delegates to {@link DefinitionTreeItem#findLeafStartsWith(String)}.
- *
- * @param text The value to find, never {@code null}.
- * @return The leaf that contains the given value, or {@code null} if
- * not found.
- */
- public DefinitionTreeItem<String> findLeafStartsWith( final String text ) {
- return getTreeRoot().findLeafStartsWith( text );
- }
-
- /**
- * Expands the node to the root, recursively.
- *
- * @param <T> The type of tree item to expand (usually String).
- * @param node The node to expand.
- */
- public <T> void expand( final TreeItem<T> node ) {
- if( node != null ) {
- expand( node.getParent() );
-
- if( !node.isLeaf() ) {
- node.setExpanded( true );
- }
- }
- }
-
- public void select( final TreeItem<String> item ) {
- getSelectionModel().clearSelection();
- getSelectionModel().select( getTreeView().getRow( item ) );
- }
-
- /**
- * Collapses the tree, recursively.
- */
- public void collapse() {
- collapse( getTreeRoot().getChildren() );
- }
-
- /**
- * Collapses the tree, recursively.
- *
- * @param <T> The type of tree item to expand (usually String).
- * @param nodes The nodes to collapse.
- */
- private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
- for( final var node : nodes ) {
- node.setExpanded( false );
- collapse( node.getChildren() );
- }
- }
-
- /**
- * @return {@code true} when the user is editing a {@link TreeItem}.
- */
- private boolean isEditingTreeItem() {
- return getTreeView().editingItemProperty().getValue() != null;
- }
-
- /**
- * Changes to edit mode for the selected item.
- */
- private void editSelectedItem() {
- getTreeView().edit( getSelectedItem() );
- }
-
- /**
- * Removes all selected items from the {@link TreeView}.
- */
- private void deleteSelectedItems() {
- for( final var item : getSelectedItems() ) {
- final var parent = item.getParent();
-
- if( parent != null ) {
- parent.getChildren().remove( item );
- }
- }
- }
-
- /**
- * Deletes the selected item.
- */
- private void deleteSelectedItem() {
- final var c = getSelectedItem();
- getSiblings( c ).remove( c );
- }
-
- /**
- * Adds a new item under the selected item (or root if nothing is selected).
- * There are a few conditions to consider: when adding to the root,
- * when adding to a leaf, and when adding to a non-leaf. Items added to the
- * root must contain two items: a key and a value.
- */
- public void addItem() {
- final var value = createTreeItem();
- getSelectedItem().getChildren().add( value );
- expand( value );
- select( value );
- }
-
- private ContextMenu createContextMenu() {
- final ContextMenu menu = new ContextMenu();
- final ObservableList<MenuItem> items = menu.getItems();
-
- addMenuItem( items, "Definition.menu.create" )
- .setOnAction( e -> addItem() );
-
- addMenuItem( items, "Definition.menu.rename" )
- .setOnAction( e -> editSelectedItem() );
-
- addMenuItem( items, "Definition.menu.remove" )
- .setOnAction( e -> deleteSelectedItem() );
-
- return menu;
- }
-
- /**
- * Executes hot-keys for edits to the definition tree.
- *
- * @param event Contains the key code of the key that was pressed.
- */
- private void keyEventFilter( final KeyEvent event ) {
- if( !isEditingTreeItem() ) {
- switch( event.getCode() ) {
- case ENTER:
- expand( getSelectedItem() );
- event.consume();
- break;
-
- case DELETE:
- deleteSelectedItems();
- break;
-
- case INSERT:
- addItem();
- break;
-
- case R:
- if( event.isControlDown() ) {
- editSelectedItem();
- }
-
- break;
- }
-
- for( final var handler : getKeyEventHandlers() ) {
- handler.handle( event );
- }
- }
- }
-
- /**
- * Adds a menu item to a list of menu items.
- *
- * @param items The list of menu items to append to.
- * @param labelKey The resource bundle key name for the menu item's label.
- * @return The menu item added to the list of menu items.
- */
- private MenuItem addMenuItem(
- final List<MenuItem> items, final String labelKey ) {
- final MenuItem menuItem = createMenuItem( labelKey );
- items.add( menuItem );
- return menuItem;
- }
-
- private MenuItem createMenuItem( final String labelKey ) {
- return new MenuItem( get( labelKey ) );
- }
-
- private DefinitionTreeItem<String> createTreeItem() {
- return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) );
- }
-
- private TreeCell<String> createTreeCell() {
- return new FocusAwareTextFieldTreeCell( createStringConverter() ) {
- @Override
- public void commitEdit( final String newValue ) {
- super.commitEdit( newValue );
- select( getTreeItem() );
- requestFocus();
- }
- };
- }
-
- @Override
- public void requestFocus() {
- super.requestFocus();
- getTreeView().requestFocus();
- }
-
- private StringConverter<String> createStringConverter() {
- return new StringConverter<>() {
- @Override
- public String toString( final String object ) {
- return object == null ? "" : object;
- }
-
- @Override
- public String fromString( final String string ) {
- return string == null ? "" : string;
- }
- };
- }
-
- /**
- * Returns the tree view that contains the definition hierarchy.
- *
- * @return A non-null instance.
- */
- public TreeView<String> getTreeView() {
- return mTreeView;
- }
-
- /**
- * Returns this pane.
- *
- * @return this
- */
- public Node getNode() {
- return this;
- }
-
- /**
- * Returns the property used to set the title of the pane: the file name.
- *
- * @return A non-null property used for showing the definition file name.
- */
- public StringProperty filenameProperty() {
- return mFilename;
- }
-
- /**
- * Returns the root of the tree.
- *
- * @return The first node added to the definition tree.
- */
- private DefinitionTreeItem<String> getTreeRoot() {
- final var root = getTreeView().getRoot();
-
- return root instanceof DefinitionTreeItem
- ? (DefinitionTreeItem<String>) root
- : new DefinitionTreeItem<>( "root" );
- }
-
- private ObservableList<TreeItem<String>> getSiblings(
- final TreeItem<String> item ) {
- final var root = getTreeView().getRoot();
- final var parent = (item == null || item == root) ? root : item.getParent();
-
- return parent.getChildren();
- }
-
- private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
- return getTreeView().getSelectionModel();
- }
-
- /**
- * Returns a copy of all the selected items.
- *
- * @return A list, possibly empty, containing all selected items in the
- * {@link TreeView}.
- */
- private List<TreeItem<String>> getSelectedItems() {
- return new ArrayList<>( getSelectionModel().getSelectedItems() );
- }
-
- public TreeItem<String> getSelectedItem() {
- final var item = getSelectionModel().getSelectedItem();
- return item == null ? getTreeView().getRoot() : item;
- }
-
- private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
- return mKeyEventHandlers;
- }
-
- /**
- * Answers whether there are any definitions in the tree.
- *
- * @return {@code true} when there are no definitions; {@code false} when
- * there's at least one definition.
- */
- public boolean isEmpty() {
- return getTreeRoot().isEmpty();
- }
-}
src/main/java/com/scrivenvar/definition/DefinitionSource.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-/**
- * Represents behaviours for reading and writing string definitions. This
- * class cannot have any direct hooks into the user interface, as it defines
- * entry points into the definition data model loaded into an object
- * hierarchy. That hierarchy is converted to a UI model using an adapter
- * pattern.
- */
-public interface DefinitionSource {
-
- /**
- * Creates an object capable of producing view-based objects from this
- * definition source.
- *
- * @return A hierarchical tree suitable for displaying in the definition pane.
- */
- TreeAdapter getTreeAdapter();
-}
src/main/java/com/scrivenvar/definition/DefinitionTreeItem.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import javafx.scene.control.TreeItem;
-
-import java.util.Stack;
-import java.util.function.BiFunction;
-
-import static java.text.Normalizer.Form.NFD;
-import static java.text.Normalizer.normalize;
-
-/**
- * Provides behaviour afforded to definition keys and corresponding value.
- *
- * @param <T> The type of {@link TreeItem} (usually string).
- */
-public class DefinitionTreeItem<T> extends TreeItem<T> {
-
- /**
- * Constructs a new item with a default value.
- *
- * @param value Passed up to superclass.
- */
- public DefinitionTreeItem( final T value ) {
- super( value );
- }
-
- /**
- * Finds a leaf starting at the current node with text that matches the given
- * value. Search is performed case-sensitively.
- *
- * @param text The text to match against each leaf in the tree.
- * @return The leaf that has a value exactly matching the given text.
- */
- public DefinitionTreeItem<T> findLeafExact( final String text ) {
- return findLeaf( text, DefinitionTreeItem::valueEquals );
- }
-
- /**
- * Finds a leaf starting at the current node with text that matches the given
- * value. Search is performed case-sensitively.
- *
- * @param text The text to match against each leaf in the tree.
- * @return The leaf that has a value that contains the given text.
- */
- public DefinitionTreeItem<T> findLeafContains( final String text ) {
- return findLeaf( text, DefinitionTreeItem::valueContains );
- }
-
- /**
- * Finds a leaf starting at the current node with text that matches the given
- * value. Search is performed case-insensitively.
- *
- * @param text The text to match against each leaf in the tree.
- * @return The leaf that has a value that contains the given text.
- */
- public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) {
- return findLeaf( text, DefinitionTreeItem::valueContainsNoCase );
- }
-
- /**
- * Finds a leaf starting at the current node with text that matches the given
- * value. Search is performed case-sensitively.
- *
- * @param text The text to match against each leaf in the tree.
- * @return The leaf that has a value that starts with the given text.
- */
- public DefinitionTreeItem<T> findLeafStartsWith( final String text ) {
- return findLeaf( text, DefinitionTreeItem::valueStartsWith );
- }
-
- /**
- * Finds a leaf starting at the current node with text that matches the given
- * value.
- *
- * @param text The text to match against each leaf in the tree.
- * @param findMode What algorithm is used to match the given text.
- * @return The leaf that has a value starting with the given text, or {@code
- * null} if there was no match found.
- */
- public DefinitionTreeItem<T> findLeaf(
- final String text,
- final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) {
- final var stack = new Stack<DefinitionTreeItem<T>>();
- stack.push( this );
-
- // Don't hunt for blank (empty) keys.
- boolean found = text.isBlank();
-
- while( !found && !stack.isEmpty() ) {
- final var node = stack.pop();
-
- for( final var child : node.getChildren() ) {
- final var result = (DefinitionTreeItem<T>) child;
-
- if( result.isLeaf() ) {
- if( found = findMode.apply( result, text ) ) {
- return result;
- }
- }
- else {
- stack.push( result );
- }
- }
- }
-
- return null;
- }
-
- /**
- * Returns the value of the string without diacritic marks.
- *
- * @return A non-null, possibly empty string.
- */
- private String getDiacriticlessValue() {
- return normalize( getValue().toString(), NFD )
- .replaceAll( "\\p{M}", "" );
- }
-
- /**
- * Returns true if this node is a leaf and its value equals the given text.
- *
- * @param s The text to compare against the node value.
- * @return true Node is a leaf and its value equals the given value.
- */
- private boolean valueEquals( final String s ) {
- return isLeaf() && getValue().equals( s );
- }
-
- /**
- * Returns true if this node is a leaf and its value contains the given text.
- *
- * @param s The text to compare against the node value.
- * @return true Node is a leaf and its value contains the given value.
- */
- private boolean valueContains( final String s ) {
- return isLeaf() && getDiacriticlessValue().contains( s );
- }
-
- /**
- * Returns true if this node is a leaf and its value contains the given text.
- *
- * @param s The text to compare against the node value.
- * @return true Node is a leaf and its value contains the given value.
- */
- private boolean valueContainsNoCase( final String s ) {
- return isLeaf() && getDiacriticlessValue()
- .toLowerCase().contains( s.toLowerCase() );
- }
-
- /**
- * Returns true if this node is a leaf and its value starts with the given
- * text.
- *
- * @param s The text to compare against the node value.
- * @return true Node is a leaf and its value starts with the given value.
- */
- private boolean valueStartsWith( final String s ) {
- return isLeaf() && getDiacriticlessValue().startsWith( s );
- }
-
- /**
- * Returns the path for this node, with nodes made distinct using the
- * separator character. This uses two loops: one for pushing nodes onto a
- * stack and one for popping them off to create the path in desired order.
- *
- * @return A non-null string, possibly empty.
- */
- public String toPath() {
- return TreeItemAdapter.toPath( getParent() );
- }
-
- /**
- * Answers whether there are any definitions in this tree.
- *
- * @return {@code true} when there are no definitions in the tree; {@code
- * false} when there is at least one definition present.
- */
- public boolean isEmpty() {
- return getChildren().isEmpty();
- }
-}
src/main/java/com/scrivenvar/definition/DocumentParser.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-/**
- * Responsible for parsing structured document formats.
- *
- * @param <T> The type of "node" for the document's object model.
- */
-public interface DocumentParser<T> {
-
- /**
- * Parses a document into a nested object hierarchy. The object returned
- * from this call must be the root node in the document tree.
- *
- * @return The document's root node, which may be empty but never null.
- */
- T getDocumentRoot();
-}
src/main/java/com/scrivenvar/definition/FocusAwareTextFieldTreeCell.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import javafx.scene.Node;
-import javafx.scene.control.TextField;
-import javafx.scene.control.cell.TextFieldTreeCell;
-import javafx.util.StringConverter;
-
-/**
- * Responsible for fixing a focus lost bug in the JavaFX implementation.
- * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details.
- * This implementation borrows from the official documentation on creating
- * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm
- */
-public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> {
- private TextField mTextField;
-
- public FocusAwareTextFieldTreeCell(
- final StringConverter<String> converter ) {
- super( converter );
- }
-
- @Override
- public void startEdit() {
- super.startEdit();
- var textField = mTextField;
-
- if( textField == null ) {
- textField = createTextField();
- }
- else {
- textField.setText( getItem() );
- }
-
- setText( null );
- setGraphic( textField );
- textField.selectAll();
- textField.requestFocus();
-
- // When the focus is lost, commit the edit then close the input field.
- // This fixes the unexpected behaviour when user clicks away.
- textField.focusedProperty().addListener( ( l, o, n ) -> {
- if( !n ) {
- commitEdit( mTextField.getText() );
- }
- } );
-
- mTextField = textField;
- }
-
- @Override
- public void cancelEdit() {
- super.cancelEdit();
- setText( getItem() );
- setGraphic( getTreeItem().getGraphic() );
- }
-
- @Override
- public void updateItem( String item, boolean empty ) {
- super.updateItem( item, empty );
-
- String text = null;
- Node graphic = null;
-
- if( !empty ) {
- if( isEditing() ) {
- final var textField = mTextField;
-
- if( textField != null ) {
- textField.setText( getString() );
- }
-
- graphic = textField;
- }
- else {
- text = getString();
- graphic = getTreeItem().getGraphic();
- }
- }
-
- setText( text );
- setGraphic( graphic );
- }
-
- private TextField createTextField() {
- final var textField = new TextField( getString() );
-
- textField.setOnKeyReleased( t -> {
- switch( t.getCode() ) {
- case ENTER -> commitEdit( textField.getText() );
- case ESCAPE -> cancelEdit();
- }
- } );
-
- return textField;
- }
-
- private String getString() {
- return getConverter().toString( getItem() );
- }
-}
src/main/java/com/scrivenvar/definition/MapInterpolator.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import com.scrivenvar.sigils.YamlSigilOperator;
-
-import java.util.Map;
-import java.util.regex.Matcher;
-
-import static com.scrivenvar.sigils.YamlSigilOperator.REGEX_PATTERN;
-
-/**
- * Responsible for performing string interpolation on key/value pairs stored
- * in a map. The values in the map can use a delimited syntax to refer to
- * keys in the map.
- */
-public class MapInterpolator {
- private static final int GROUP_DELIMITED = 1;
-
- /**
- * Empty.
- */
- private MapInterpolator() {
- }
-
- /**
- * Performs string interpolation on the values in the given map. This will
- * change any value in the map that contains a variable that matches
- * {@link YamlSigilOperator#REGEX_PATTERN}.
- *
- * @param map Contains values that represent references to keys.
- */
- public static void interpolate( final Map<String, String> map ) {
- map.replaceAll( ( k, v ) -> resolve( map, v ) );
- }
-
- /**
- * Given a value with zero or more key references, this will resolve all
- * the values, recursively. If a key cannot be dereferenced, the value will
- * contain the key name.
- *
- * @param map Map to search for keys when resolving key references.
- * @param value Value containing zero or more key references
- * @return The given value with all embedded key references interpolated.
- */
- private static String resolve(
- final Map<String, String> map, String value ) {
- final Matcher matcher = REGEX_PATTERN.matcher( value );
-
- while( matcher.find() ) {
- final String keyName = matcher.group( GROUP_DELIMITED );
- final String mapValue = map.get( keyName );
- final String keyValue = mapValue == null
- ? keyName
- : resolve( map, mapValue );
-
- value = value.replace( keyName, keyValue );
- }
-
- return value;
- }
-}
src/main/java/com/scrivenvar/definition/RootTreeItem.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import javafx.scene.control.TreeItem;
-import javafx.scene.control.TreeView;
-
-/**
- * Indicates that this is the top-most {@link TreeItem}. This class allows
- * the {@link TreeItemAdapter} to ignore the topmost definition. Such
- * contortions are necessary because {@link TreeView} requires a root item
- * that isn't part of the user's definition file.
- * <p>
- * Another approach would be to associate object pairs per {@link TreeItem},
- * but that would be a waste of memory since the only "exception" case is
- * the root {@link TreeItem}.
- * </p>
- *
- * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
- */
-public class RootTreeItem<T> extends DefinitionTreeItem<T> {
- /**
- * Default constructor, calls the superclass, no other behaviour.
- *
- * @param value The {@link TreeItem} node name to construct the superclass.
- * @see TreeItemAdapter#toMap(TreeItem) for details on how this
- * class is used.
- */
- public RootTreeItem( final T value ) {
- super( value );
- }
-}
src/main/java/com/scrivenvar/definition/TreeAdapter.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import javafx.scene.control.TreeItem;
-
-import java.io.IOException;
-import java.nio.file.Path;
-
-/**
- * Responsible for converting an object hierarchy into a {@link TreeItem}
- * hierarchy.
- */
-public interface TreeAdapter {
- /**
- * Adapts the document produced by the given parser into a {@link TreeItem}
- * object that can be presented to the user within a GUI.
- *
- * @param root The default root node name.
- * @return The parsed document in a {@link TreeItem} that can be displayed
- * in a panel.
- */
- TreeItem<String> adapt( String root );
-
- /**
- * Exports the given root node to the given path.
- *
- * @param root The root node to export.
- * @param path Where to persist the data.
- * @throws IOException Could not write the data to the given path.
- */
- void export( TreeItem<String> root, Path path ) throws IOException;
-}
src/main/java/com/scrivenvar/definition/TreeItemAdapter.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.scrivenvar.sigils.YamlSigilOperator;
-import com.scrivenvar.preview.HTMLPreviewPane;
-import javafx.scene.control.TreeItem;
-import javafx.scene.control.TreeView;
-
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Stack;
-
-import static com.scrivenvar.Constants.DEFAULT_MAP_SIZE;
-
-/**
- * Given a {@link TreeItem}, this will generate a flat map with all the
- * values in the tree recursively interpolated. The application integrates
- * definition files as follows:
- * <ol>
- * <li>Load YAML file into {@link JsonNode} hierarchy.</li>
- * <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
- * <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
- * <li>Substitute flat map variables into document as required.</li>
- * </ol>
- *
- * <p>
- * This class is responsible for producing the interpolated flat map. This
- * allows dynamic edits of the {@link TreeView} to be displayed in the
- * {@link HTMLPreviewPane} without having to reload the definition file.
- * Reloading the definition file would work, but has a number of drawbacks.
- * </p>
- */
-public class TreeItemAdapter {
- /**
- * Separates YAML definition keys (e.g., the dots in {@code $root.node.var$}).
- */
- public static final String SEPARATOR = ".";
-
- /**
- * Default buffer length for keys ({@link StringBuilder} has 16 character
- * buffer) that should be large enough for most keys to avoid reallocating
- * memory to increase the {@link StringBuilder}'s buffer.
- */
- public static final int DEFAULT_KEY_LENGTH = 64;
-
- /**
- * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
- * as a consecutive list.
- */
- private static final class TreeIterator
- implements Iterator<TreeItem<String>> {
- private final Stack<TreeItem<String>> mStack = new Stack<>();
-
- public TreeIterator( final TreeItem<String> root ) {
- if( root != null ) {
- mStack.push( root );
- }
- }
-
- @Override
- public boolean hasNext() {
- return !mStack.isEmpty();
- }
-
- @Override
- public TreeItem<String> next() {
- final TreeItem<String> next = mStack.pop();
- next.getChildren().forEach( mStack::push );
-
- return next;
- }
- }
-
- private TreeItemAdapter() {
- }
-
- /**
- * Iterate over a given root node (at any level of the tree) and process each
- * leaf node into a flat map. Values must be interpolated separately.
- */
- public static Map<String, String> toMap( final TreeItem<String> root ) {
- final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
- final TreeIterator iterator = new TreeIterator( root );
-
- iterator.forEachRemaining( item -> {
- if( item.isLeaf() ) {
- map.put( toPath( item.getParent() ), item.getValue() );
- }
- } );
-
- return map;
- }
-
-
- /**
- * For a given node, this will ascend the tree to generate a key name
- * that is associated with the leaf node's value.
- *
- * @param node Ascendants represent the key to this node's value.
- * @param <T> Data type that the {@link TreeItem} contains.
- * @return The string representation of the node's unique key.
- */
- public static <T> String toPath( TreeItem<T> node ) {
- assert node != null;
-
- final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH );
- final Stack<TreeItem<T>> stack = new Stack<>();
-
- while( node != null && !(node instanceof RootTreeItem) ) {
- stack.push( node );
- node = node.getParent();
- }
-
- // Gets set at end of first iteration (to avoid an if condition).
- String separator = "";
-
- while( !stack.empty() ) {
- final T subkey = stack.pop().getValue();
- key.append( separator );
- key.append( subkey );
- separator = SEPARATOR;
- }
-
- return YamlSigilOperator.entoken( key.toString() );
- }
-}
src/main/java/com/scrivenvar/definition/yaml/YamlDefinitionSource.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition.yaml;
-
-import com.scrivenvar.definition.DefinitionSource;
-import com.scrivenvar.definition.TreeAdapter;
-
-import java.nio.file.Path;
-
-/**
- * Represents a definition data source for YAML files.
- */
-public class YamlDefinitionSource implements DefinitionSource {
-
- private final YamlTreeAdapter mYamlTreeAdapter;
-
- /**
- * Constructs a new YAML definition source, populated from the given file.
- *
- * @param path Path to the YAML definition file.
- */
- public YamlDefinitionSource( final Path path ) {
- assert path != null;
-
- mYamlTreeAdapter = new YamlTreeAdapter( path );
- }
-
- @Override
- public TreeAdapter getTreeAdapter() {
- return mYamlTreeAdapter;
- }
-}
src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition.yaml;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
-import com.scrivenvar.definition.DocumentParser;
-
-import java.io.InputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-/**
- * Responsible for reading a YAML document into an object hierarchy.
- */
-public class YamlParser implements DocumentParser<JsonNode> {
-
- /**
- * Start of the Universe (the YAML document node that contains all others).
- */
- private final JsonNode mDocumentRoot;
-
- /**
- * Creates a new YamlParser instance that attempts to parse the contents
- * of the YAML document given from a path. In the event that the file either
- * does not exist or is empty, a fake
- *
- * @param path Path to a file containing YAML data to parse.
- */
- public YamlParser( final Path path ) {
- assert path != null;
- mDocumentRoot = parse( path );
- }
-
- /**
- * Returns the parent node for the entire YAML document tree.
- *
- * @return The document root, never {@code null}.
- */
- @Override
- public JsonNode getDocumentRoot() {
- return mDocumentRoot;
- }
-
- /**
- * Parses the given path containing YAML data into an object hierarchy.
- *
- * @param path {@link Path} to the YAML resource to parse.
- * @return The parsed contents, or an empty object hierarchy.
- */
- private JsonNode parse( final Path path ) {
- try( final InputStream in = Files.newInputStream( path ) ) {
- return new ObjectMapper( new YAMLFactory() ).readTree( in );
- } catch( final Exception e ) {
- // Ensure that a document root node exists by relying on the
- // default failure condition when processing. This is required
- // because the input stream could not be read.
- return new ObjectMapper().createObjectNode();
- }
- }
-}
src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.definition.yaml;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
-import com.scrivenvar.definition.RootTreeItem;
-import com.scrivenvar.definition.TreeAdapter;
-import com.scrivenvar.definition.DefinitionTreeItem;
-import javafx.scene.control.TreeItem;
-import javafx.scene.control.TreeView;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Map.Entry;
-
-/**
- * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
- * interface and vice-versa.
- */
-public class YamlTreeAdapter implements TreeAdapter {
- private final YamlParser mParser;
-
- /**
- * Constructs a new instance that will use the given path to read
- * the object hierarchy from a data source.
- *
- * @param path Path to YAML contents to parse.
- */
- public YamlTreeAdapter( final Path path ) {
- mParser = new YamlParser( path );
- }
-
- @Override
- public void export( final TreeItem<String> treeItem, final Path path )
- throws IOException {
- final YAMLMapper mapper = new YAMLMapper();
- final ObjectNode root = mapper.createObjectNode();
-
- // Iterate over the root item's children. The root item is used by the
- // application to ensure definitions can always be added to a tree, as
- // such it is not meant to be exported, only its children.
- for( final TreeItem<String> child : treeItem.getChildren() ) {
- export( child, root );
- }
-
- // Writes as UTF8 by default.
- mapper.writeValue( path.toFile(), root );
- }
-
- /**
- * Recursive method to generate an object hierarchy that represents the
- * given {@link TreeItem} hierarchy.
- *
- * @param item The {@link TreeItem} to reproduce as an object hierarchy.
- * @param node The {@link ObjectNode} to update to reflect the
- * {@link TreeItem} hierarchy.
- */
- private void export( final TreeItem<String> item, ObjectNode node ) {
- final var children = item.getChildren();
-
- // If the current item has more than one non-leaf child, it's an
- // object node and must become a new nested object.
- if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
- node = node.putObject( item.getValue() );
- }
-
- for( final TreeItem<String> child : children ) {
- if( child.isLeaf() ) {
- node.put( item.getValue(), child.getValue() );
- }
- else {
- export( child, node );
- }
- }
- }
-
- /**
- * Converts a YAML document to a {@link TreeItem} based on the document
- * keys. Only the first document in the stream is adapted.
- *
- * @param root Root {@link TreeItem} node name.
- * @return A {@link TreeItem} populated with all the keys in the YAML
- * document.
- */
- public TreeItem<String> adapt( final String root ) {
- final JsonNode rootNode = getYamlParser().getDocumentRoot();
- final TreeItem<String> rootItem = createRootTreeItem( root );
-
- rootItem.setExpanded( true );
- adapt( rootNode, rootItem );
- return rootItem;
- }
-
- /**
- * Iterate over a given root node (at any level of the tree) and adapt each
- * leaf node.
- *
- * @param rootNode A JSON node (YAML node) to adapt.
- * @param rootItem The tree item to use as the root when processing the node.
- */
- private void adapt(
- final JsonNode rootNode, final TreeItem<String> rootItem ) {
- rootNode.fields().forEachRemaining(
- ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
- );
- }
-
- /**
- * Recursively adapt each rootNode to a corresponding rootItem.
- *
- * @param rootNode The node to adapt.
- * @param rootItem The item to adapt using the node's key.
- */
- private void adapt(
- final Entry<String, JsonNode> rootNode,
- final TreeItem<String> rootItem ) {
- final JsonNode leafNode = rootNode.getValue();
- final String key = rootNode.getKey();
- final TreeItem<String> leaf = createTreeItem( key );
-
- if( leafNode.isValueNode() ) {
- leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
- }
-
- rootItem.getChildren().add( leaf );
-
- if( leafNode.isObject() ) {
- adapt( leafNode, leaf );
- }
- }
-
- /**
- * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
- *
- * @param value The node's value.
- * @return A new {@link TreeItem}, never {@code null}.
- */
- private TreeItem<String> createTreeItem( final String value ) {
- return new DefinitionTreeItem<>( value );
- }
-
- /**
- * Creates a new {@link TreeItem} that is intended to be the root-level item
- * added to the {@link TreeView}. This allows the root item to be
- * distinguished from the other items so that reference keys do not include
- * "Definition" as part of their name.
- *
- * @param value The node's value.
- * @return A new {@link TreeItem}, never {@code null}.
- */
- private TreeItem<String> createRootTreeItem( final String value ) {
- return new RootTreeItem<>( value );
- }
-
- public YamlParser getYamlParser() {
- return mParser;
- }
-}
src/main/java/com/scrivenvar/dialogs/AbstractDialog.java
-/*
- * Copyright 2017 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.dialogs;
-
-import static com.scrivenvar.Messages.get;
-import com.scrivenvar.service.events.impl.ButtonOrderPane;
-import static javafx.scene.control.ButtonType.CANCEL;
-import static javafx.scene.control.ButtonType.OK;
-import javafx.scene.control.Dialog;
-import javafx.stage.Window;
-
-/**
- * Superclass that abstracts common behaviours for all dialogs.
- *
- * @param <T> The type of dialog to create (usually String).
- */
-public abstract class AbstractDialog<T> extends Dialog<T> {
-
- /**
- * Ensures that all dialogs can be closed.
- *
- * @param owner The parent window of this dialog.
- * @param title The messages title to display in the title bar.
- */
- @SuppressWarnings( "OverridableMethodCallInConstructor" )
- public AbstractDialog( final Window owner, final String title ) {
- setTitle( get( title ) );
- setResizable( true );
-
- initOwner( owner );
- initCloseAction();
- initDialogPane();
- initDialogButtons();
- initComponents();
- }
-
- /**
- * Initialize the component layout.
- */
- protected abstract void initComponents();
-
- /**
- * Set the dialog to use a button order pane with an OK and a CANCEL button.
- */
- protected void initDialogPane() {
- setDialogPane( new ButtonOrderPane() );
- }
-
- /**
- * Set an OK and CANCEL button on the dialog.
- */
- protected void initDialogButtons() {
- getDialogPane().getButtonTypes().addAll( OK, CANCEL );
- }
-
- /**
- * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
- * can always close the window, even if there's an error.
- */
- protected final void initCloseAction() {
- final Window window = getDialogPane().getScene().getWindow();
- window.setOnCloseRequest( event -> window.hide() );
- }
-}
src/main/java/com/scrivenvar/dialogs/ImageDialog.java
-/*
- * Copyright 2015 Karl Tauber <karl at jformdesigner dot com>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.dialogs;
-
-import static com.scrivenvar.Messages.get;
-import com.scrivenvar.controls.BrowseFileButton;
-import com.scrivenvar.controls.EscapeTextField;
-import java.nio.file.Path;
-import javafx.application.Platform;
-import javafx.beans.binding.Bindings;
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-import javafx.scene.control.ButtonBar.ButtonData;
-import static javafx.scene.control.ButtonType.OK;
-import javafx.scene.control.DialogPane;
-import javafx.scene.control.Label;
-import javafx.stage.FileChooser.ExtensionFilter;
-import javafx.stage.Window;
-import org.tbee.javafx.scene.layout.fxml.MigPane;
-
-/**
- * Dialog to enter a markdown image.
- */
-public class ImageDialog extends AbstractDialog<String> {
-
- private final StringProperty image = new SimpleStringProperty();
-
- public ImageDialog( final Window owner, final Path basePath ) {
- super(owner, "Dialog.image.title" );
-
- final DialogPane dialogPane = getDialogPane();
- dialogPane.setContent( pane );
-
- linkBrowseFileButton.setBasePath( basePath );
- linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
- linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
-
- dialogPane.lookupButton( OK ).disableProperty().bind(
- urlField.escapedTextProperty().isEmpty()
- .or( textField.escapedTextProperty().isEmpty() ) );
-
- image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
- .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
- .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
- previewField.textProperty().bind( image );
-
- setResultConverter( dialogButton -> {
- ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
- return (data == ButtonData.OK_DONE) ? image.get() : null;
- } );
-
- Platform.runLater( () -> {
- urlField.requestFocus();
-
- if( urlField.getText().startsWith( "http://" ) ) {
- urlField.selectRange( "http://".length(), urlField.getLength() );
- }
- } );
- }
-
- @Override
- protected void initComponents() {
- // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
- pane = new MigPane();
- Label urlLabel = new Label();
- urlField = new EscapeTextField();
- linkBrowseFileButton = new BrowseFileButton();
- Label textLabel = new Label();
- textField = new EscapeTextField();
- Label titleLabel = new Label();
- titleField = new EscapeTextField();
- Label previewLabel = new Label();
- previewField = new Label();
-
- //======== pane ========
- {
- pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
- pane.setRows( "[][][][]" );
-
- //---- urlLabel ----
- urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
- pane.add( urlLabel, "cell 0 0" );
-
- //---- urlField ----
- urlField.setEscapeCharacters( "()" );
- urlField.setText( "http://yourlink.com" );
- urlField.setPromptText( "http://yourlink.com" );
- pane.add( urlField, "cell 1 0" );
- pane.add( linkBrowseFileButton, "cell 2 0" );
-
- //---- textLabel ----
- textLabel.setText( get( "Dialog.image.textLabel.text" ) );
- pane.add( textLabel, "cell 0 1" );
-
- //---- textField ----
- textField.setEscapeCharacters( "[]" );
- pane.add( textField, "cell 1 1 2 1" );
-
- //---- titleLabel ----
- titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
- pane.add( titleLabel, "cell 0 2" );
- pane.add( titleField, "cell 1 2 2 1" );
-
- //---- previewLabel ----
- previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
- pane.add( previewLabel, "cell 0 3" );
- pane.add( previewField, "cell 1 3 2 1" );
- }
- // JFormDesigner - End of component initialization //GEN-END:initComponents
- }
-
- // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
- private MigPane pane;
- private EscapeTextField urlField;
- private BrowseFileButton linkBrowseFileButton;
- private EscapeTextField textField;
- private EscapeTextField titleField;
- private Label previewField;
- // JFormDesigner - End of variables declaration //GEN-END:variables
-}
src/main/java/com/scrivenvar/dialogs/ImageDialog.jfd
-JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
-
-new FormModel {
- "i18n.bundlePackage": "com.scrivendor"
- "i18n.bundleName": "messages"
- "i18n.autoExternalize": true
- "i18n.keyPrefix": "ImageDialog"
- contentType: "form/javafx"
- root: new FormRoot {
- add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
- "$layoutConstraints": ""
- "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
- "$rowConstraints": "[][][][]"
- } ) {
- name: "pane"
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "urlLabel"
- "text": new FormMessage( null, "ImageDialog.urlLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 0"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "urlField"
- "escapeCharacters": "()"
- "text": "http://yourlink.com"
- "promptText": "http://yourlink.com"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 0"
- } )
- add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
- name: "linkBrowseFileButton"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 2 0"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "textLabel"
- "text": new FormMessage( null, "ImageDialog.textLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 1"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "textField"
- "escapeCharacters": "[]"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 1 2 1"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "titleLabel"
- "text": new FormMessage( null, "ImageDialog.titleLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 2"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "titleField"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 2 2 1"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "previewLabel"
- "text": new FormMessage( null, "ImageDialog.previewLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 3"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "previewField"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 3 2 1"
- } )
- }, new FormLayoutConstraints( null ) {
- "location": new javafx.geometry.Point2D( 0.0, 0.0 )
- "size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
- } )
- }
-}
src/main/java/com/scrivenvar/dialogs/LinkDialog.java
-/*
- * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.dialogs;
-
-import com.scrivenvar.controls.EscapeTextField;
-import com.scrivenvar.editors.markdown.HyperlinkModel;
-import javafx.application.Platform;
-import javafx.beans.binding.Bindings;
-import javafx.beans.property.SimpleStringProperty;
-import javafx.beans.property.StringProperty;
-import javafx.scene.control.ButtonBar.ButtonData;
-import javafx.scene.control.DialogPane;
-import javafx.scene.control.Label;
-import javafx.stage.Window;
-import org.tbee.javafx.scene.layout.fxml.MigPane;
-
-import static com.scrivenvar.Messages.get;
-import static javafx.scene.control.ButtonType.OK;
-
-/**
- * Dialog to enter a markdown link.
- */
-public class LinkDialog extends AbstractDialog<String> {
-
- private final StringProperty link = new SimpleStringProperty();
-
- public LinkDialog(
- final Window owner, final HyperlinkModel hyperlink ) {
- super( owner, "Dialog.link.title" );
-
- final DialogPane dialogPane = getDialogPane();
- dialogPane.setContent( pane );
-
- dialogPane.lookupButton( OK ).disableProperty().bind(
- urlField.escapedTextProperty().isEmpty() );
-
- textField.setText( hyperlink.getText() );
- urlField.setText( hyperlink.getUrl() );
- titleField.setText( hyperlink.getTitle() );
-
- link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
- .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
- .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
- .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
- .otherwise( urlField.escapedTextProperty() ) ) );
-
- setResultConverter( dialogButton -> {
- ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
- return (data == ButtonData.OK_DONE) ? link.get() : null;
- } );
-
- Platform.runLater( () -> {
- urlField.requestFocus();
- urlField.selectRange( 0, urlField.getLength() );
- } );
- }
-
- @Override
- protected void initComponents() {
- // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents
- pane = new MigPane();
- Label urlLabel = new Label();
- urlField = new EscapeTextField();
- Label textLabel = new Label();
- textField = new EscapeTextField();
- Label titleLabel = new Label();
- titleField = new EscapeTextField();
-
- //======== pane ========
- {
- pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
- pane.setRows( "[][][][]" );
-
- //---- urlLabel ----
- urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
- pane.add( urlLabel, "cell 0 0" );
-
- //---- urlField ----
- urlField.setEscapeCharacters( "()" );
- pane.add( urlField, "cell 1 0" );
-
- //---- textLabel ----
- textLabel.setText( get( "Dialog.link.textLabel.text" ) );
- pane.add( textLabel, "cell 0 1" );
-
- //---- textField ----
- textField.setEscapeCharacters( "[]" );
- pane.add( textField, "cell 1 1 3 1" );
-
- //---- titleLabel ----
- titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
- pane.add( titleLabel, "cell 0 2" );
- pane.add( titleField, "cell 1 2 3 1" );
- }
- // JFormDesigner - End of component initialization //GEN-END:initComponents
- }
-
- // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables
- private MigPane pane;
- private EscapeTextField urlField;
- private EscapeTextField textField;
- private EscapeTextField titleField;
- // JFormDesigner - End of variables declaration //GEN-END:variables
-}
src/main/java/com/scrivenvar/dialogs/LinkDialog.jfd
-JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
-
-new FormModel {
- "i18n.bundlePackage": "com.scrivendor"
- "i18n.bundleName": "messages"
- "i18n.autoExternalize": true
- "i18n.keyPrefix": "LinkDialog"
- contentType: "form/javafx"
- root: new FormRoot {
- add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
- "$layoutConstraints": ""
- "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
- "$rowConstraints": "[][][][]"
- } ) {
- name: "pane"
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "urlLabel"
- "text": new FormMessage( null, "LinkDialog.urlLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 0"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "urlField"
- "escapeCharacters": "()"
- "text": "http://yourlink.com"
- "promptText": "http://yourlink.com"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 0"
- } )
- add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
- name: "linkBrowseDirectoyButton"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 2 0"
- } )
- add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
- name: "linkBrowseFileButton"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 3 0"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "textLabel"
- "text": new FormMessage( null, "LinkDialog.textLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 1"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "textField"
- "escapeCharacters": "[]"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 1 3 1"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "titleLabel"
- "text": new FormMessage( null, "LinkDialog.titleLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 2"
- } )
- add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
- name: "titleField"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 2 3 1"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "previewLabel"
- "text": new FormMessage( null, "LinkDialog.previewLabel.text" )
- auxiliary() {
- "JavaCodeGenerator.variableLocal": true
- }
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 0 3"
- } )
- add( new FormComponent( "javafx.scene.control.Label" ) {
- name: "previewField"
- }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
- "value": "cell 1 3 3 1"
- } )
- }, new FormLayoutConstraints( null ) {
- "location": new javafx.geometry.Point2D( 0.0, 0.0 )
- "size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
- } )
- }
-}
src/main/java/com/scrivenvar/editors/DefinitionDecoratorFactory.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.editors;
-
-import com.scrivenvar.AbstractFileFactory;
-import com.scrivenvar.sigils.RSigilOperator;
-import com.scrivenvar.sigils.SigilOperator;
-import com.scrivenvar.sigils.YamlSigilOperator;
-
-import java.nio.file.Path;
-
-/**
- * Responsible for creating a definition name decorator suited to a particular
- * file type.
- */
-public class DefinitionDecoratorFactory extends AbstractFileFactory {
-
- private DefinitionDecoratorFactory() {
- }
-
- public static SigilOperator newInstance( final Path path ) {
- final var factory = new DefinitionDecoratorFactory();
-
- return switch( factory.lookup( path ) ) {
- case RMARKDOWN, RXML -> new RSigilOperator();
- default -> new YamlSigilOperator();
- };
- }
-}
src/main/java/com/scrivenvar/editors/DefinitionNameInjector.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.editors;
-
-import com.scrivenvar.FileEditorTab;
-import com.scrivenvar.definition.DefinitionPane;
-import com.scrivenvar.definition.DefinitionTreeItem;
-import com.scrivenvar.sigils.SigilOperator;
-import javafx.scene.control.TreeItem;
-import javafx.scene.input.KeyEvent;
-import org.fxmisc.richtext.StyledTextArea;
-
-import java.nio.file.Path;
-import java.text.BreakIterator;
-
-import static com.scrivenvar.Constants.*;
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static java.lang.Character.isWhitespace;
-import static javafx.scene.input.KeyCode.SPACE;
-import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-
-/**
- * Provides the logic for injecting variable names within the editor.
- */
-public final class DefinitionNameInjector {
-
- /**
- * Recipient of name injections.
- */
- private FileEditorTab mTab;
-
- /**
- * Initiates double-click events.
- */
- private final DefinitionPane mDefinitionPane;
-
- /**
- * Initializes the variable name injector against the given pane.
- *
- * @param pane The definition panel to listen to for double-click events.
- */
- public DefinitionNameInjector( final DefinitionPane pane ) {
- mDefinitionPane = pane;
- }
-
- /**
- * Trap Control+Space.
- *
- * @param tab Editor where variable names get injected.
- */
- public void addListener( final FileEditorTab tab ) {
- assert tab != null;
- mTab = tab;
-
- tab.getEditorPane().addKeyboardListener(
- keyPressed( SPACE, CONTROL_DOWN ),
- this::autoinsert
- );
- }
-
- /**
- * Inserts the currently selected variable from the {@link DefinitionPane}.
- */
- public void injectSelectedItem() {
- final var pane = getDefinitionPane();
- final TreeItem<String> item = pane.getSelectedItem();
-
- if( item.isLeaf() ) {
- final var leaf = pane.findLeafExact( item.getValue() );
- final var editor = getEditor();
-
- editor.insertText( editor.getCaretPosition(), decorate( leaf ) );
- }
- }
-
- /**
- * Pressing Control+SPACE will find a node that matches the current word and
- * substitute the definition reference.
- */
- public void autoinsert() {
- final String paragraph = getCaretParagraph();
- final int[] bounds = getWordBoundariesAtCaret();
-
- try {
- if( isEmptyDefinitionPane() ) {
- alert( STATUS_DEFINITION_EMPTY );
- }
- else {
- final String word = paragraph.substring( bounds[ 0 ], bounds[ 1 ] );
-
- if( word.isBlank() ) {
- alert( STATUS_DEFINITION_BLANK );
- }
- else {
- final var leaf = findLeaf( word );
-
- if( leaf == null ) {
- alert( STATUS_DEFINITION_MISSING, word );
- }
- else {
- replaceText( bounds[ 0 ], bounds[ 1 ], decorate( leaf ) );
- expand( leaf );
- }
- }
- }
- } catch( final Exception ignored ) {
- alert( STATUS_DEFINITION_BLANK );
- }
- }
-
- /**
- * Pressing Control+SPACE will find a node that matches the current word and
- * substitute the definition reference.
- *
- * @param e Ignored -- it can only be Control+SPACE.
- */
- private void autoinsert( final KeyEvent e ) {
- autoinsert();
- }
-
- /**
- * Finds the start and end indexes for the word in the current paragraph
- * where the caret is located. There are a few different scenarios, where
- * the caret can be at: the start, end, or middle of a word; also, the
- * caret can be at the end or beginning of a punctuated word; as well, the
- * caret could be at the beginning or end of the line or document.
- */
- private int[] getWordBoundariesAtCaret() {
- final var paragraph = getCaretParagraph();
- final var length = paragraph.length();
- int offset = getCurrentCaretColumn();
-
- int began = offset;
- int ended = offset;
-
- while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
- began--;
- }
-
- while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
- ended++;
- }
-
- final var iterator = BreakIterator.getWordInstance();
- iterator.setText( paragraph );
-
- while( began < length && iterator.isBoundary( began + 1 ) ) {
- began++;
- }
-
- while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
- ended--;
- }
-
- return new int[]{began, ended};
- }
-
- /**
- * Decorates a {@link TreeItem} using the syntax specific to the type of
- * document being edited.
- *
- * @param leaf The path to the leaf (the definition key) to be decorated.
- */
- private String decorate( final DefinitionTreeItem<String> leaf ) {
- return decorate( leaf.toPath() );
- }
-
- /**
- * Decorates a variable using the syntax specific to the type of document
- * being edited.
- *
- * @param variable The variable to decorate in dot-notation without any
- * start or end sigils present.
- */
- private String decorate( final String variable ) {
- return getVariableDecorator().apply( variable );
- }
-
- /**
- * Updates the text at the given position within the current paragraph.
- *
- * @param posBegan The starting index in the paragraph text to replace.
- * @param posEnded The ending index in the paragraph text to replace.
- * @param text Overwrite the paragraph substring with this text.
- */
- private void replaceText(
- final int posBegan, final int posEnded, final String text ) {
- final int p = getCurrentParagraph();
-
- getEditor().replaceText( p, posBegan, p, posEnded, text );
- }
-
- /**
- * Returns the caret's current paragraph position.
- *
- * @return A number greater than or equal to 0.
- */
- private int getCurrentParagraph() {
- return getEditor().getCurrentParagraph();
- }
-
- /**
- * Returns the text for the paragraph that contains the caret.
- *
- * @return A non-null string, possibly empty.
- */
- private String getCaretParagraph() {
- return getEditor().getText( getCurrentParagraph() );
- }
-
- /**
- * Returns the caret position within the current paragraph.
- *
- * @return A value from 0 to the length of the current paragraph.
- */
- private int getCurrentCaretColumn() {
- return getEditor().getCaretColumn();
- }
-
- /**
- * Looks for the given word, matching first by exact, next by a starts-with
- * condition with diacritics replaced, then by containment.
- *
- * @param word The word to match by: exact, at the beginning, or containment.
- * @return The matching {@link DefinitionTreeItem} for the given word, or
- * {@code null} if none found.
- */
- @SuppressWarnings("ConstantConditions")
- private DefinitionTreeItem<String> findLeaf( final String word ) {
- assert word != null;
-
- final var pane = getDefinitionPane();
- DefinitionTreeItem<String> leaf = null;
-
- leaf = leaf == null ? pane.findLeafExact( word ) : leaf;
- leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf;
- leaf = leaf == null ? pane.findLeafContains( word ) : leaf;
- leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf;
-
- return leaf;
- }
-
- /**
- * Answers whether there are any definitions in the tree.
- *
- * @return {@code true} when there are no definitions; {@code false} when
- * there's at least one definition.
- */
- private boolean isEmptyDefinitionPane() {
- return getDefinitionPane().isEmpty();
- }
-
- /**
- * Collapses the tree then expands and selects the given node.
- *
- * @param node The node to expand.
- */
- private void expand( final TreeItem<String> node ) {
- final DefinitionPane pane = getDefinitionPane();
- pane.collapse();
- pane.expand( node );
- pane.select( node );
- }
-
- /**
- * @return A variable decorator that corresponds to the given file type.
- */
- private SigilOperator getVariableDecorator() {
- return DefinitionDecoratorFactory.newInstance( getFilename() );
- }
-
- private Path getFilename() {
- return getFileEditorTab().getPath();
- }
-
- private EditorPane getEditorPane() {
- return getFileEditorTab().getEditorPane();
- }
-
- private StyledTextArea<?, ?> getEditor() {
- return getEditorPane().getEditor();
- }
-
- public FileEditorTab getFileEditorTab() {
- return mTab;
- }
-
- private DefinitionPane getDefinitionPane() {
- return mDefinitionPane;
- }
-}
src/main/java/com/scrivenvar/editors/EditorPane.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.editors;
-
-import com.scrivenvar.preferences.UserPreferences;
-import javafx.beans.property.IntegerProperty;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.SimpleObjectProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.event.Event;
-import javafx.scene.control.ScrollPane;
-import javafx.scene.layout.Pane;
-import org.fxmisc.flowless.VirtualizedScrollPane;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import org.fxmisc.undo.UndoManager;
-import org.fxmisc.wellbehaved.event.EventPattern;
-import org.fxmisc.wellbehaved.event.Nodes;
-
-import java.nio.file.Path;
-import java.util.function.Consumer;
-
-import static com.scrivenvar.StatusBarNotifier.clearAlert;
-import static java.lang.String.format;
-import static javafx.application.Platform.runLater;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-
-/**
- * Represents common editing features for various types of text editors.
- */
-public class EditorPane extends Pane {
-
- /**
- * Used when changing the text area font size.
- */
- private static final String FMT_CSS_FONT_SIZE = "-fx-font-size: %dpt;";
-
- private final StyleClassedTextArea mEditor =
- new StyleClassedTextArea( false );
- private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
- new VirtualizedScrollPane<>( mEditor );
- private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>();
-
- public EditorPane() {
- getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
- fontsSizeProperty().addListener(
- ( l, o, n ) -> setFontSize( n.intValue() )
- );
-
- // Clear out any previous alerts after the user has typed. If the problem
- // persists, re-rendering the document will re-raise the error. If there
- // was no previous error, clearing the alert is essentially a no-op.
- mEditor.textProperty().addListener(
- ( l, o, n ) -> clearAlert()
- );
- }
-
- @Override
- public void requestFocus() {
- requestFocus( 3 );
- }
-
- /**
- * There's a race-condition between displaying the {@link EditorPane}
- * and giving the {@link #mEditor} focus. Try to focus up to {@code max}
- * times before giving up.
- *
- * @param max The number of attempts to try to request focus.
- */
- private void requestFocus( final int max ) {
- if( max > 0 ) {
- runLater(
- () -> {
- final var editor = getEditor();
-
- if( !editor.isFocused() ) {
- editor.requestFocus();
- requestFocus( max - 1 );
- }
- }
- );
- }
- }
-
- public void undo() {
- getUndoManager().undo();
- }
-
- public void redo() {
- getUndoManager().redo();
- }
-
- /**
- * Cuts the actively selected text; if no text is selected, this will cut
- * the entire paragraph.
- */
- public void cut() {
- final var editor = getEditor();
- final var selected = editor.getSelectedText();
-
- if( selected == null || selected.isEmpty() ) {
- editor.selectParagraph();
- }
-
- editor.cut();
- }
-
- public void copy() {
- getEditor().copy();
- }
-
- public void paste() {
- getEditor().paste();
- }
-
- public void selectAll() {
- getEditor().selectAll();
- }
-
- public UndoManager<?> getUndoManager() {
- return getEditor().getUndoManager();
- }
-
- public String getText() {
- return getEditor().getText();
- }
-
- public void setText( final String text ) {
- final var editor = getEditor();
- editor.deselect();
- editor.replaceText( text );
- getUndoManager().mark();
- }
-
- /**
- * Call to hook into changes to the text area.
- *
- * @param listener Receives editor text change events.
- */
- public void addTextChangeListener(
- final ChangeListener<? super String> listener ) {
- getEditor().textProperty().addListener( listener );
- }
-
- /**
- * Notifies observers when the caret changes paragraph.
- *
- * @param listener Receives change event.
- */
- public void addCaretParagraphListener(
- final ChangeListener<? super Integer> listener ) {
- getEditor().currentParagraphProperty().addListener( listener );
- }
-
- /**
- * Notifies observers when the caret changes position.
- *
- * @param listener Receives change event.
- */
- public void addCaretPositionListener(
- final ChangeListener<? super Integer> listener ) {
- getEditor().caretPositionProperty().addListener( listener );
- }
-
- /**
- * This method adds listeners to editor events.
- *
- * @param <T> The event type.
- * @param <U> The consumer type for the given event type.
- * @param event The event of interest.
- * @param consumer The method to call when the event happens.
- */
- public <T extends Event, U extends T> void addKeyboardListener(
- final EventPattern<? super T, ? extends U> event,
- final Consumer<? super U> consumer ) {
- Nodes.addInputMap( getEditor(), consume( event, consumer ) );
- }
-
- /**
- * Repositions the cursor and scroll bar to the top of the file.
- */
- public void scrollToTop() {
- getEditor().moveTo( 0 );
- getScrollPane().scrollYToPixel( 0 );
- }
-
- public StyleClassedTextArea getEditor() {
- return mEditor;
- }
-
- /**
- * Returns the scroll pane that contains the text area.
- *
- * @return The scroll pane that contains the content to edit.
- */
- public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
- return mScrollPane;
- }
-
- public Path getPath() {
- return mPath.get();
- }
-
- public void setPath( final Path path ) {
- mPath.set( path );
- }
-
- /**
- * Sets the font size in points.
- *
- * @param size The new font size to use for the text editor.
- */
- private void setFontSize( final int size ) {
- mEditor.setStyle( format( FMT_CSS_FONT_SIZE, size ) );
- }
-
- /**
- * Returns the text editor font size property for handling font size change
- * events.
- */
- private IntegerProperty fontsSizeProperty() {
- return UserPreferences.getInstance().fontsSizeEditorProperty();
- }
-}
src/main/java/com/scrivenvar/editors/markdown/HyperlinkModel.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.editors.markdown;
-
-import com.vladsch.flexmark.ast.Link;
-
-/**
- * Represents the model for a hyperlink: text, url, and title.
- */
-public class HyperlinkModel {
-
- private String text;
- private String url;
- private String title;
-
- /**
- * Constructs a new hyperlink model in Markdown format by default with no
- * title (i.e., tooltip).
- *
- * @param text The hyperlink text displayed (e.g., displayed to the user).
- * @param url The destination URL (e.g., when clicked).
- */
- public HyperlinkModel( final String text, final String url ) {
- this( text, url, null );
- }
-
- /**
- * Constructs a new hyperlink model for the given AST link.
- *
- * @param link A markdown link.
- */
- public HyperlinkModel( final Link link ) {
- this(
- link.getText().toString(),
- link.getUrl().toString(),
- link.getTitle().toString()
- );
- }
-
- /**
- * Constructs a new hyperlink model in Markdown format by default.
- *
- * @param text The hyperlink text displayed (e.g., displayed to the user).
- * @param url The destination URL (e.g., when clicked).
- * @param title The hyperlink title (e.g., shown as a tooltip).
- */
- public HyperlinkModel( final String text, final String url,
- final String title ) {
- setText( text );
- setUrl( url );
- setTitle( title );
- }
-
- /**
- * Returns the string in Markdown format by default.
- *
- * @return A markdown version of the hyperlink.
- */
- @Override
- public String toString() {
- String format = "%s%s%s";
-
- if( hasText() ) {
- format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
- }
-
- // Becomes ""+URL+"" if no text is set.
- // Becomes [TITLE]+(URL)+"" if no title is set.
- // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
- return String.format( format, getText(), getUrl(), getTitle() );
- }
-
- public final void setText( final String text ) {
- this.text = nullSafe( text );
- }
-
- public final void setUrl( final String url ) {
- this.url = nullSafe( url );
- }
-
- public final void setTitle( final String title ) {
- this.title = nullSafe( title );
- }
-
- /**
- * Answers whether text has been set for the hyperlink.
- *
- * @return true This is a text link.
- */
- public boolean hasText() {
- return !getText().isEmpty();
- }
-
- /**
- * Answers whether a title (tooltip) has been set for the hyperlink.
- *
- * @return true There is a title.
- */
- public boolean hasTitle() {
- return !getTitle().isEmpty();
- }
-
- public String getText() {
- return this.text;
- }
-
- public String getUrl() {
- return this.url;
- }
-
- public String getTitle() {
- return this.title;
- }
-
- private String nullSafe( final String s ) {
- return s == null ? "" : s;
- }
-}
src/main/java/com/scrivenvar/editors/markdown/LinkVisitor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.editors.markdown;
-
-import com.vladsch.flexmark.ast.Link;
-import com.vladsch.flexmark.util.ast.Node;
-import com.vladsch.flexmark.util.ast.NodeVisitor;
-import com.vladsch.flexmark.util.ast.VisitHandler;
-
-/**
- * Responsible for extracting a hyperlink from the document so that the user
- * can edit the link within a dialog.
- */
-public class LinkVisitor {
-
- private NodeVisitor visitor;
- private Link link;
- private final int offset;
-
- /**
- * Creates a hyperlink given an offset into a paragraph and the markdown AST
- * link node.
- *
- * @param index Index into the paragraph that indicates the hyperlink to
- * change.
- */
- public LinkVisitor( final int index ) {
- this.offset = index;
- }
-
- public Link process( final Node root ) {
- getVisitor().visit( root );
- return getLink();
- }
-
- /**
- * @param link Not null.
- */
- private void visit( final Link link ) {
- final int began = link.getStartOffset();
- final int ended = link.getEndOffset();
- final int index = getOffset();
-
- if( index >= began && index <= ended ) {
- setLink( link );
- }
- }
-
- private synchronized NodeVisitor getVisitor() {
- if( this.visitor == null ) {
- this.visitor = createVisitor();
- }
-
- return this.visitor;
- }
-
- protected NodeVisitor createVisitor() {
- return new NodeVisitor(
- new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
- }
-
- private Link getLink() {
- return this.link;
- }
-
- private void setLink( final Link link ) {
- this.link = link;
- }
-
- public int getOffset() {
- return this.offset;
- }
-}
src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.editors.markdown;
-
-import com.scrivenvar.dialogs.ImageDialog;
-import com.scrivenvar.dialogs.LinkDialog;
-import com.scrivenvar.editors.EditorPane;
-import com.scrivenvar.processors.markdown.BlockExtension;
-import com.scrivenvar.processors.markdown.MarkdownProcessor;
-import com.vladsch.flexmark.ast.Link;
-import com.vladsch.flexmark.html.renderer.AttributablePart;
-import com.vladsch.flexmark.util.ast.Node;
-import com.vladsch.flexmark.util.html.MutableAttributes;
-import javafx.scene.control.Dialog;
-import javafx.scene.control.IndexRange;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyEvent;
-import javafx.stage.Window;
-import org.fxmisc.richtext.StyleClassedTextArea;
-
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
-import static com.scrivenvar.util.Utils.ltrim;
-import static com.scrivenvar.util.Utils.rtrim;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-
-/**
- * Provides the ability to edit a text document.
- */
-public class MarkdownEditorPane extends EditorPane {
- private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
- "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
-
- /**
- * Any of these followed by a space and a letter produce a line
- * by themselves. The ">" need not be followed by a space.
- */
- private static final Pattern PATTERN_NEW_LINE = Pattern.compile(
- "^>|(((#+)|([*+\\-])|([1-9]\\.))\\s+).+" );
-
- public MarkdownEditorPane() {
- initEditor();
- }
-
- private void initEditor() {
- final StyleClassedTextArea textArea = getEditor();
-
- textArea.setWrapText( true );
- textArea.getStyleClass().add( "markdown-editor" );
- textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
-
- addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
- addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut );
- }
-
- public void insertLink() {
- insertObject( createLinkDialog() );
- }
-
- public void insertImage() {
- insertObject( createImageDialog() );
- }
-
- /**
- * Returns the editor's paragraph number that will be close to its HTML
- * paragraph ID. Ultimately this solution is flawed because there isn't
- * a straightforward correlation between the document being edited and
- * what is rendered. XML documents transformed through stylesheets have
- * no readily determined correlation. Images, tables, and other
- * objects affect the relative location of the current paragraph being
- * edited with respect to the preview pane.
- * <p>
- * See
- * {@link BlockExtension.IdAttributeProvider#setAttributes(Node, AttributablePart, MutableAttributes)}}
- * for details.
- * </p>
- * <p>
- * Injecting a token into the document, as per a previous version of the
- * application, can instruct the preview pane where to shift the viewport.
- * </p>
- *
- * @param paraIndex The paragraph index from the editor pane to scroll to
- * in the preview pane, which will be approximated if an
- * equivalent cannot be found.
- * @return A unique identifier that correlates to an equivalent paragraph
- * number once the Markdown is rendered into HTML.
- */
- public int approximateParagraphId( final int paraIndex ) {
- final StyleClassedTextArea editor = getEditor();
- final List<String> lines = new ArrayList<>( 4096 );
-
- int i = 0;
- String prevText = "";
- boolean withinFencedBlock = false;
- boolean withinCodeBlock = false;
-
- for( final var p : editor.getParagraphs() ) {
- if( i > paraIndex ) {
- break;
- }
-
- final String text = p.getText().replace( '>', ' ' );
- if( text.startsWith( "```" ) ) {
- if( withinFencedBlock = !withinFencedBlock ) {
- lines.add( text );
- }
- }
-
- if( !withinFencedBlock ) {
- final boolean foundCodeBlock = text.startsWith( " " );
-
- if( foundCodeBlock && !withinCodeBlock ) {
- lines.add( text );
- withinCodeBlock = true;
- }
- else if( !foundCodeBlock ) {
- withinCodeBlock = false;
- }
- }
-
- if( !withinFencedBlock && !withinCodeBlock &&
- ((!text.isBlank() && prevText.isBlank()) ||
- PATTERN_NEW_LINE.matcher( text ).matches()) ) {
- lines.add( text );
- }
-
- prevText = text;
- i++;
- }
-
- // Scrolling index is 1-based.
- return Math.max( lines.size() - 1, 0 );
- }
-
- /**
- * Gets the index of the paragraph where the caret is positioned.
- *
- * @return The paragraph number for the caret.
- */
- public int getCurrentParagraphIndex() {
- return getEditor().getCurrentParagraph();
- }
-
- /**
- * @param leading Characters to insert at the beginning of the current
- * selection (or paragraph).
- * @param trailing Characters to insert at the end of the current selection
- * (or paragraph).
- */
- public void surroundSelection( final String leading, final String trailing ) {
- surroundSelection( leading, trailing, null );
- }
-
- /**
- * @param leading Characters to insert at the beginning of the current
- * selection (or paragraph).
- * @param trailing Characters to insert at the end of the current selection
- * (or paragraph).
- * @param hint Instructional text inserted within the leading and
- * trailing characters, provided no text is selected.
- */
- public void surroundSelection(
- String leading, String trailing, final String hint ) {
- final StyleClassedTextArea textArea = getEditor();
-
- // Note: not using textArea.insertText() to insert leading and trailing
- // because this would add two changes to undo history
- final IndexRange selection = textArea.getSelection();
- int start = selection.getStart();
- int end = selection.getEnd();
-
- final String selectedText = textArea.getSelectedText();
-
- String trimmedText = selectedText.trim();
- if( trimmedText.length() < selectedText.length() ) {
- start += selectedText.indexOf( trimmedText );
- end = start + trimmedText.length();
- }
-
- // remove leading whitespaces from leading text if selection starts at zero
- if( start == 0 ) {
- leading = ltrim( leading );
- }
-
- // remove trailing whitespaces from trailing text if selection ends at
- // text end
- if( end == textArea.getLength() ) {
- trailing = rtrim( trailing );
- }
-
- // remove leading line separators from leading text
- // if there are line separators before the selected text
- if( leading.startsWith( "\n" ) ) {
- for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
- if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
- break;
- }
-
- leading = leading.substring( 1 );
- }
- }
-
- // remove trailing line separators from trailing or leading text
- // if there are line separators after the selected text
- final boolean trailingIsEmpty = trailing.isEmpty();
- String str = trailingIsEmpty ? leading : trailing;
-
- if( str.endsWith( "\n" ) ) {
- final int length = textArea.getLength();
-
- for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
- if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
- break;
- }
-
- str = str.substring( 0, str.length() - 1 );
- }
-
- if( trailingIsEmpty ) {
- leading = str;
- }
- else {
- trailing = str;
- }
- }
-
- int selStart = start + leading.length();
- int selEnd = end + leading.length();
-
- // insert hint text if selection is empty
- if( hint != null && trimmedText.isEmpty() ) {
- trimmedText = hint;
- selEnd = selStart + hint.length();
- }
-
- // prevent undo merging with previous text entered by user
- getUndoManager().preventMerge();
-
- // replace text and update selection
- textArea.replaceText( start, end, leading + trimmedText + trailing );
- textArea.selectRange( selStart, selEnd );
- }
-
- private void enterPressed( final KeyEvent e ) {
- final StyleClassedTextArea textArea = getEditor();
- final String currentLine =
- textArea.getText( textArea.getCurrentParagraph() );
- final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
-
- String newText = "\n";
-
- if( matcher.matches() ) {
- if( !matcher.group( 2 ).isEmpty() ) {
- // indent new line with same whitespace characters and list markers
- // as current line
- newText = newText.concat( matcher.group( 1 ) );
- }
- else {
- // current line contains only whitespace characters and list markers
- // --> empty current line
- final int caretPosition = textArea.getCaretPosition();
- textArea.selectRange( caretPosition - currentLine.length(),
- caretPosition );
- }
- }
-
- textArea.replaceSelection( newText );
-
- // Ensure that the window scrolls when Enter is pressed at the bottom of
- // the pane.
- textArea.requestFollowCaret();
- }
-
- private void cut( final KeyEvent event ) {
- super.cut();
- }
-
- /**
- * Returns one of: selected text, word under cursor, or parsed hyperlink from
- * the markdown AST.
- *
- * @return An instance containing the link URL and display text.
- */
- private HyperlinkModel getHyperlink() {
- final StyleClassedTextArea textArea = getEditor();
- final String selectedText = textArea.getSelectedText();
-
- // Get the current paragraph, convert to Markdown nodes.
- final MarkdownProcessor mp = new MarkdownProcessor( null );
- final int p = textArea.getCurrentParagraph();
- final String paragraph = textArea.getText( p );
- final Node node = mp.toNode( paragraph );
- final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
- final Link link = visitor.process( node );
-
- if( link != null ) {
- textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
- }
-
- return createHyperlinkModel(
- link, selectedText, "https://localhost"
- );
- }
-
- @SuppressWarnings("SameParameterValue")
- private HyperlinkModel createHyperlinkModel(
- final Link link, final String selection, final String url ) {
-
- return link == null
- ? new HyperlinkModel( selection, url )
- : new HyperlinkModel( link );
- }
-
- private Path getParentPath() {
- final Path path = getPath();
- return (path != null) ? path.getParent() : null;
- }
-
- private Dialog<String> createLinkDialog() {
- return new LinkDialog( getWindow(), getHyperlink() );
- }
-
- private Dialog<String> createImageDialog() {
- return new ImageDialog( getWindow(), getParentPath() );
- }
-
- private void insertObject( final Dialog<String> dialog ) {
- dialog.showAndWait().ifPresent(
- result -> getEditor().replaceSelection( result )
- );
- }
-
- private Window getWindow() {
- return getScrollPane().getScene().getWindow();
- }
-}
src/main/java/com/scrivenvar/predicates/PredicateFactory.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.predicates;
-
-import java.io.File;
-import java.util.Collection;
-import java.util.function.Predicate;
-
-import static java.lang.String.join;
-import static java.nio.file.FileSystems.getDefault;
-
-/**
- * Provides a number of simple {@link Predicate} instances for various types
- * of string comparisons, including basic strings and file name strings.
- */
-public class PredicateFactory {
- /**
- * Creates an instance of {@link Predicate} that matches a globbed file
- * name pattern.
- *
- * @param pattern The file name pattern to match.
- * @return A {@link Predicate} that can answer whether a given file name
- * matches the given glob pattern.
- */
- public static Predicate<File> createFileTypePredicate(
- final String pattern ) {
- final var matcher = getDefault().getPathMatcher(
- "glob:**{" + pattern + "}"
- );
-
- return file -> matcher.matches( file.toPath() );
- }
-
- /**
- * Creates an instance of {@link Predicate} that matches any file name from
- * a {@link Collection} of file name patterns. The given patterns are joined
- * with commas into a single comma-separated list.
- *
- * @param patterns The file name patterns to be matched.
- * @return A {@link Predicate} that can answer whether a given file name
- * matches the given glob patterns.
- */
- public static Predicate<File> createFileTypePredicate(
- final Collection<String> patterns ) {
- return createFileTypePredicate( join( ",", patterns ) );
- }
-
- /**
- * Creates an instance of {@link Predicate} that compares whether the given
- * {@code reference} string is contained by the comparator. Comparison is
- * case-insensitive. The test will also pass if the comparate is empty.
- *
- * @param comparator The string to check as being contained.
- * @return A {@link Predicate} that can answer whether the given string
- * is contained within the comparator, or the comparate is empty.
- */
- public static Predicate<String> createStringContainsPredicate(
- final String comparator ) {
- return comparate -> comparate.isEmpty() ||
- comparate.toLowerCase().contains( comparator.toLowerCase() );
- }
- /**
- * Creates an instance of {@link Predicate} that compares whether the given
- * {@code reference} string is starts with the comparator. Comparison is
- * case-insensitive.
- *
- * @param comparator The string to check as being contained.
- * @return A {@link Predicate} that can answer whether the given string
- * is contained within the comparator.
- */
- public static Predicate<String> createStringStartsPredicate(
- final String comparator ) {
- return comparate ->
- comparate.toLowerCase().startsWith( comparator.toLowerCase() );
- }
-}
src/main/java/com/scrivenvar/preferences/FilePreferences.java
-/*
- * Copyright 2016 David Croft and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preferences;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.util.*;
-import java.util.prefs.AbstractPreferences;
-import java.util.prefs.BackingStoreException;
-
-import static com.scrivenvar.StatusBarNotifier.alert;
-
-/**
- * Preferences implementation that stores to a user-defined file. Local file
- * storage is preferred over a certain operating system's monolithic trash heap
- * called a registry. When the OS is locked down, the default Preferences
- * implementation will try to write to the registry and fail due to permissions
- * problems. This class sidesteps the issue entirely by writing to the user's
- * home directory, where permissions should be a bit more lax.
- */
-public class FilePreferences extends AbstractPreferences {
-
- private final Map<String, String> mRoot = new TreeMap<>();
- private final Map<String, FilePreferences> mChildren = new TreeMap<>();
- private boolean mRemoved;
-
- private final Object mMutex = new Object();
-
- public FilePreferences(
- final AbstractPreferences parent, final String name ) {
- super( parent, name );
-
- try {
- sync();
- } catch( final BackingStoreException ex ) {
- alert( ex );
- }
- }
-
- @Override
- protected void putSpi( final String key, final String value ) {
- synchronized( mMutex ) {
- mRoot.put( key, value );
- }
-
- try {
- flush();
- } catch( final BackingStoreException ex ) {
- alert( ex );
- }
- }
-
- @Override
- protected String getSpi( final String key ) {
- synchronized( mMutex ) {
- return mRoot.get( key );
- }
- }
-
- @Override
- protected void removeSpi( final String key ) {
- synchronized( mMutex ) {
- mRoot.remove( key );
- }
-
- try {
- flush();
- } catch( final BackingStoreException ex ) {
- alert( ex );
- }
- }
-
- @Override
- protected void removeNodeSpi() throws BackingStoreException {
- mRemoved = true;
- flush();
- }
-
- @Override
- protected String[] keysSpi() {
- synchronized( mMutex ) {
- return mRoot.keySet().toArray( new String[ 0 ] );
- }
- }
-
- @Override
- protected String[] childrenNamesSpi() {
- return mChildren.keySet().toArray( new String[ 0 ] );
- }
-
- @Override
- protected FilePreferences childSpi( final String name ) {
- FilePreferences child = mChildren.get( name );
-
- if( child == null || child.isRemoved() ) {
- child = new FilePreferences( this, name );
- mChildren.put( name, child );
- }
-
- return child;
- }
-
- @Override
- protected void syncSpi() {
- if( isRemoved() ) {
- return;
- }
-
- final File file = FilePreferencesFactory.getPreferencesFile();
-
- if( !file.exists() ) {
- return;
- }
-
- synchronized( mMutex ) {
- final Properties p = new Properties();
-
- try( final var inputStream = new FileInputStream( file ) ) {
- p.load( inputStream );
-
- final String path = getPath();
- final Enumeration<?> propertyNames = p.propertyNames();
-
- while( propertyNames.hasMoreElements() ) {
- final String propKey = (String) propertyNames.nextElement();
-
- if( propKey.startsWith( path ) ) {
- final String subKey = propKey.substring( path.length() );
-
- // Only load immediate descendants
- if( subKey.indexOf( '.' ) == -1 ) {
- mRoot.put( subKey, p.getProperty( propKey ) );
- }
- }
- }
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
- }
-
- private String getPath() {
- final FilePreferences parent = (FilePreferences) parent();
-
- return parent == null ? "" : parent.getPath() + name() + '.';
- }
-
- @Override
- protected void flushSpi() {
- final File file = FilePreferencesFactory.getPreferencesFile();
-
- synchronized( mMutex ) {
- final Properties p = new Properties();
-
- try {
- final String path = getPath();
-
- if( file.exists() ) {
- try( final var fis = new FileInputStream( file ) ) {
- p.load( fis );
- }
-
- final List<String> toRemove = new ArrayList<>();
-
- // Make a list of all direct children of this node to be removed
- final Enumeration<?> propertyNames = p.propertyNames();
-
- while( propertyNames.hasMoreElements() ) {
- final String propKey = (String) propertyNames.nextElement();
- if( propKey.startsWith( path ) ) {
- final String subKey = propKey.substring( path.length() );
-
- // Only do immediate descendants
- if( subKey.indexOf( '.' ) == -1 ) {
- toRemove.add( propKey );
- }
- }
- }
-
- // Remove them now that the enumeration is done with
- for( final String propKey : toRemove ) {
- p.remove( propKey );
- }
- }
-
- // If this node hasn't been removed, add back in any values
- if( !mRemoved ) {
- for( final String s : mRoot.keySet() ) {
- p.setProperty( path + s, mRoot.get( s ) );
- }
- }
-
- try( final var fos = new FileOutputStream( file ) ) {
- p.store( fos, "FilePreferences" );
- }
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
- }
-}
src/main/java/com/scrivenvar/preferences/FilePreferencesFactory.java
-/*
- * Copyright 2016 David Croft and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preferences;
-
-import java.io.File;
-import java.nio.file.FileSystems;
-import java.util.prefs.Preferences;
-import java.util.prefs.PreferencesFactory;
-
-import static com.scrivenvar.Constants.APP_TITLE;
-
-/**
- * PreferencesFactory implementation that stores the preferences in a
- * user-defined file. Usage:
- * <pre>
- * System.setProperty( "java.util.prefs.PreferencesFactory",
- * FilePreferencesFactory.class.getName() );
- * </pre>
- * <p>
- * The file defaults to <code>$user.home/.scrivenvar</code>, but can be changed
- * using <code>-Dapplication.name=preferences</code> when running the
- * application, or by calling <code>System.setProperty</code> with the
- * "application.name" property.
- * </p>
- */
-public class FilePreferencesFactory implements PreferencesFactory {
-
- private static File preferencesFile;
- private Preferences rootPreferences;
-
- @Override
- public Preferences systemRoot() {
- return userRoot();
- }
-
- @Override
- public synchronized Preferences userRoot() {
- if( rootPreferences == null ) {
- rootPreferences = new FilePreferences( null, "" );
- }
-
- return rootPreferences;
- }
-
- public synchronized static File getPreferencesFile() {
- if( preferencesFile == null ) {
- String prefsFile = getPreferencesFilename();
-
- preferencesFile = new File( prefsFile ).getAbsoluteFile();
- }
-
- return preferencesFile;
- }
-
- public static String getPreferencesFilename() {
- final String filename = System.getProperty( "application.name", APP_TITLE );
- return System.getProperty( "user.home" ) + getSeparator() + "." + filename;
- }
-
- public static String getSeparator() {
- return FileSystems.getDefault().getSeparator();
- }
-}
src/main/java/com/scrivenvar/preferences/UserPreferences.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preferences;
-
-import com.dlsc.formsfx.model.structure.StringField;
-import com.dlsc.preferencesfx.PreferencesFx;
-import com.dlsc.preferencesfx.PreferencesFxEvent;
-import com.dlsc.preferencesfx.model.Category;
-import com.dlsc.preferencesfx.model.Group;
-import com.dlsc.preferencesfx.model.Setting;
-import javafx.beans.property.*;
-import javafx.event.EventHandler;
-import javafx.scene.Node;
-import javafx.scene.control.Label;
-
-import java.io.File;
-import java.nio.file.Path;
-
-import static com.scrivenvar.Constants.*;
-import static com.scrivenvar.Messages.get;
-
-/**
- * Responsible for user preferences that can be changed from the GUI. The
- * settings are displayed and persisted using {@link PreferencesFx}.
- */
-public class UserPreferences {
- /**
- * Implementation of the initialization-on-demand holder design pattern,
- * an for a lazy-loaded singleton. In all versions of Java, the idiom enables
- * a safe, highly concurrent lazy initialization of static fields with good
- * performance. The implementation relies upon the initialization phase of
- * execution within the Java Virtual Machine (JVM) as specified by the Java
- * Language Specification. When the class {@link UserPreferencesContainer}
- * is loaded, its initialization completes trivially because there are no
- * static variables to initialize.
- * <p>
- * The static class definition {@link UserPreferencesContainer} within the
- * {@link UserPreferences} is not initialized until such time that
- * {@link UserPreferencesContainer} must be executed. The static
- * {@link UserPreferencesContainer} class executes when
- * {@link #getInstance} is called. The first call will trigger loading and
- * initialization of the {@link UserPreferencesContainer} thereby
- * instantiating the {@link #INSTANCE}.
- * </p>
- * <p>
- * This indirection is necessary because the {@link UserPreferences} class
- * references {@link PreferencesFx}, which must not be instantiated until the
- * UI is ready.
- * </p>
- */
- private static class UserPreferencesContainer {
- private static final UserPreferences INSTANCE = new UserPreferences();
- }
-
- public static UserPreferences getInstance() {
- return UserPreferencesContainer.INSTANCE;
- }
-
- private final PreferencesFx mPreferencesFx;
-
- private final ObjectProperty<File> mPropRDirectory;
- private final StringProperty mPropRScript;
- private final ObjectProperty<File> mPropImagesDirectory;
- private final StringProperty mPropImagesOrder;
- private final ObjectProperty<File> mPropDefinitionPath;
- private final StringProperty mRDelimiterBegan;
- private final StringProperty mRDelimiterEnded;
- private final StringProperty mDefDelimiterBegan;
- private final StringProperty mDefDelimiterEnded;
- private final IntegerProperty mPropFontsSizeEditor;
-
- private UserPreferences() {
- mPropRDirectory = simpleFile( USER_DIRECTORY );
- mPropRScript = new SimpleStringProperty( "" );
-
- mPropImagesDirectory = simpleFile( USER_DIRECTORY );
- mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT );
-
- mPropDefinitionPath = simpleFile(
- getSetting( "file.definition.default", DEFINITION_NAME )
- );
-
- mDefDelimiterBegan = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT );
- mDefDelimiterEnded = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT );
-
- mRDelimiterBegan = new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT );
- mRDelimiterEnded = new SimpleStringProperty( R_DELIM_ENDED_DEFAULT );
-
- mPropFontsSizeEditor = new SimpleIntegerProperty( (int) FONT_SIZE_EDITOR );
-
- // All properties must be initialized before creating the dialog.
- mPreferencesFx = createPreferencesFx();
- }
-
- /**
- * Display the user preferences settings dialog (non-modal).
- */
- public void show() {
- getPreferencesFx().show( false );
- }
-
- /**
- * Call to persist the settings. Strictly speaking, this could watch on
- * all values for external changes then save automatically.
- */
- public void save() {
- getPreferencesFx().saveSettings();
- }
-
- /**
- * Creates the preferences dialog.
- * <p>
- * TODO: Make this dynamic by iterating over all "Preferences.*" values
- * that follow a particular naming pattern.
- * </p>
- *
- * @return A new instance of preferences for users to edit.
- */
- @SuppressWarnings("unchecked")
- private PreferencesFx createPreferencesFx() {
- final Setting<StringField, StringProperty> scriptSetting =
- Setting.of( "Script", mPropRScript );
- final StringField field = scriptSetting.getElement();
- field.multiline( true );
-
- return PreferencesFx.of(
- UserPreferences.class,
- Category.of(
- get( "Preferences.r" ),
- Group.of(
- get( "Preferences.r.directory" ),
- Setting.of( label( "Preferences.r.directory.desc", false ) ),
- Setting.of( "Directory", mPropRDirectory, true )
- ),
- Group.of(
- get( "Preferences.r.script" ),
- Setting.of( label( "Preferences.r.script.desc" ) ),
- scriptSetting
- ),
- Group.of(
- get( "Preferences.r.delimiter.began" ),
- Setting.of( label( "Preferences.r.delimiter.began.desc" ) ),
- Setting.of( "Opening", mRDelimiterBegan )
- ),
- Group.of(
- get( "Preferences.r.delimiter.ended" ),
- Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ),
- Setting.of( "Closing", mRDelimiterEnded )
- )
- ),
- Category.of(
- get( "Preferences.images" ),
- Group.of(
- get( "Preferences.images.directory" ),
- Setting.of( label( "Preferences.images.directory.desc" ) ),
- Setting.of( "Directory", mPropImagesDirectory, true )
- ),
- Group.of(
- get( "Preferences.images.suffixes" ),
- Setting.of( label( "Preferences.images.suffixes.desc" ) ),
- Setting.of( "Extensions", mPropImagesOrder )
- )
- ),
- Category.of(
- get( "Preferences.definitions" ),
- Group.of(
- get( "Preferences.definitions.path" ),
- Setting.of( label( "Preferences.definitions.path.desc" ) ),
- Setting.of( "Path", mPropDefinitionPath, false )
- ),
- Group.of(
- get( "Preferences.definitions.delimiter.began" ),
- Setting.of( label(
- "Preferences.definitions.delimiter.began.desc" ) ),
- Setting.of( "Opening", mDefDelimiterBegan )
- ),
- Group.of(
- get( "Preferences.definitions.delimiter.ended" ),
- Setting.of( label(
- "Preferences.definitions.delimiter.ended.desc" ) ),
- Setting.of( "Closing", mDefDelimiterEnded )
- )
- ),
- Category.of(
- get( "Preferences.fonts" ),
- Group.of(
- get( "Preferences.fonts.size_editor" ),
- Setting.of( label( "Preferences.fonts.size_editor.desc" ) ),
- Setting.of( "Points", mPropFontsSizeEditor )
- )
- )
- ).instantPersistent( false );
- }
-
- /**
- * Wraps a {@link File} inside a {@link SimpleObjectProperty}.
- *
- * @param path The file name to use when constructing the {@link File}.
- * @return A new {@link SimpleObjectProperty} instance with a {@link File}
- * that references the given {@code path}.
- */
- private SimpleObjectProperty<File> simpleFile( final String path ) {
- return new SimpleObjectProperty<>( new File( path ) );
- }
-
- /**
- * Creates a label for the given key after interpolating its value.
- *
- * @param key The key to find in the resource bundle.
- * @return The value of the key as a label.
- */
- private Node label( final String key ) {
- return new Label( get( key, true ) );
- }
-
- /**
- * Creates a label for the given key.
- *
- * @param key The key to find in the resource bundle.
- * @param interpolate {@code true} means to interpolate the value.
- * @return The value of the key, interpolated if {@code interpolate} is
- * {@code true}.
- */
- @SuppressWarnings("SameParameterValue")
- private Node label( final String key, final boolean interpolate ) {
- return new Label( get( key, interpolate ) );
- }
-
- /**
- * Delegates to the {@link PreferencesFx} event handler for monitoring
- * save events.
- *
- * @param eventHandler The handler to call when the preferences are saved.
- */
- public void addSaveEventHandler(
- final EventHandler<? super PreferencesFxEvent> eventHandler ) {
- final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED;
- getPreferencesFx().addEventHandler( eventType, eventHandler );
- }
-
- /**
- * Returns the value for a key from the settings properties file.
- *
- * @param key Key within the settings properties file to find.
- * @param value Default value to return if the key is not found.
- * @return The value for the given key from the settings file, or the
- * given {@code value} if no key found.
- */
- @SuppressWarnings("SameParameterValue")
- private String getSetting( final String key, final String value ) {
- return SETTINGS.getSetting( key, value );
- }
-
- public ObjectProperty<File> definitionPathProperty() {
- return mPropDefinitionPath;
- }
-
- public Path getDefinitionPath() {
- return definitionPathProperty().getValue().toPath();
- }
-
- private StringProperty defDelimiterBegan() {
- return mDefDelimiterBegan;
- }
-
- public String getDefDelimiterBegan() {
- return defDelimiterBegan().get();
- }
-
- private StringProperty defDelimiterEnded() {
- return mDefDelimiterEnded;
- }
-
- public String getDefDelimiterEnded() {
- return defDelimiterEnded().get();
- }
-
- public ObjectProperty<File> rDirectoryProperty() {
- return mPropRDirectory;
- }
-
- public File getRDirectory() {
- return rDirectoryProperty().getValue();
- }
-
- public StringProperty rScriptProperty() {
- return mPropRScript;
- }
-
- public String getRScript() {
- return rScriptProperty().getValue();
- }
-
- private StringProperty rDelimiterBegan() {
- return mRDelimiterBegan;
- }
-
- public String getRDelimiterBegan() {
- return rDelimiterBegan().get();
- }
-
- private StringProperty rDelimiterEnded() {
- return mRDelimiterEnded;
- }
-
- public String getRDelimiterEnded() {
- return rDelimiterEnded().get();
- }
-
- private ObjectProperty<File> imagesDirectoryProperty() {
- return mPropImagesDirectory;
- }
-
- public File getImagesDirectory() {
- return imagesDirectoryProperty().getValue();
- }
-
- private StringProperty imagesOrderProperty() {
- return mPropImagesOrder;
- }
-
- public String getImagesOrder() {
- return imagesOrderProperty().getValue();
- }
-
- public IntegerProperty fontsSizeEditorProperty() {
- return mPropFontsSizeEditor;
- }
-
- /**
- * Returns the preferred font size of the text editor.
- *
- * @return A non-negative integer, in points.
- */
- public int getFontsSizeEditor() {
- return mPropFontsSizeEditor.intValue();
- }
-
- private PreferencesFx getPreferencesFx() {
- return mPreferencesFx;
- }
-}
src/main/java/com/scrivenvar/preview/ChainedReplacedElementFactory.java
-/*
- * Copyright 2006 Patrick Wright
- * Copyright 2007 Wisconsin Court System
- * Copyright 2020 White Magic Software, Ltd.
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public License
- * as published by the Free Software Foundation; either version 2.1
- * of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- */
-package com.scrivenvar.preview;
-
-import com.scrivenvar.adapters.ReplacedElementAdapter;
-import org.w3c.dom.Element;
-import org.xhtmlrenderer.extend.ReplacedElement;
-import org.xhtmlrenderer.extend.ReplacedElementFactory;
-import org.xhtmlrenderer.extend.UserAgentCallback;
-import org.xhtmlrenderer.layout.LayoutContext;
-import org.xhtmlrenderer.render.BlockBox;
-
-import java.util.HashSet;
-import java.util.Set;
-
-public class ChainedReplacedElementFactory extends ReplacedElementAdapter {
- private final Set<ReplacedElementFactory> mFactoryList = new HashSet<>();
-
- @Override
- public ReplacedElement createReplacedElement(
- final LayoutContext c,
- final BlockBox box,
- final UserAgentCallback uac,
- final int cssWidth,
- final int cssHeight ) {
- for( final var f : mFactoryList ) {
- final var r = f.createReplacedElement(
- c, box, uac, cssWidth, cssHeight );
-
- if( r != null ) {
- return r;
- }
- }
-
- return null;
- }
-
- @Override
- public void reset() {
- for( final var factory : mFactoryList ) {
- factory.reset();
- }
- }
-
- @Override
- public void remove( final Element element ) {
- for( final var factory : mFactoryList ) {
- factory.remove( element );
- }
- }
-
- public void addFactory( final ReplacedElementFactory factory ) {
- mFactoryList.add( factory );
- }
-}
src/main/java/com/scrivenvar/preview/CustomImageLoader.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preview;
-
-import javafx.beans.property.IntegerProperty;
-import javafx.beans.property.SimpleIntegerProperty;
-import org.xhtmlrenderer.extend.FSImage;
-import org.xhtmlrenderer.resource.ImageResource;
-import org.xhtmlrenderer.swing.ImageResourceLoader;
-
-import javax.imageio.ImageIO;
-import java.net.URI;
-import java.net.URL;
-import java.nio.file.Paths;
-
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static com.scrivenvar.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER;
-import static com.scrivenvar.util.ProtocolResolver.getProtocol;
-import static java.lang.String.valueOf;
-import static java.nio.file.Files.exists;
-import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
-
-/**
- * Responsible for loading images. If the image cannot be found, a placeholder
- * is used instead.
- */
-public class CustomImageLoader extends ImageResourceLoader {
- /**
- * Placeholder that's displayed when image cannot be found.
- */
- private FSImage mBrokenImage;
-
- private final IntegerProperty mWidthProperty = new SimpleIntegerProperty();
-
- /**
- * Gets an {@link IntegerProperty} that represents the maximum width an
- * image should be scaled.
- *
- * @return The maximum width for an image.
- */
- public IntegerProperty widthProperty() {
- return mWidthProperty;
- }
-
- /**
- * Gets an image resolved from the given URI. If the image cannot be found,
- * this will return a custom placeholder image indicating the reference
- * is broken.
- *
- * @param uri Path to the image resource to load.
- * @param width Ignored.
- * @param height Ignored.
- * @return The scaled image, or a placeholder image if the URI's content
- * could not be retrieved.
- */
- @Override
- public synchronized ImageResource get(
- final String uri, final int width, final int height ) {
- assert uri != null;
- assert width >= 0;
- assert height >= 0;
-
- try {
- final var protocol = getProtocol( uri );
- final ImageResource imageResource;
-
- if( protocol.isFile() && exists( Paths.get( new URI( uri ) ) ) ) {
- imageResource = super.get( uri, width, height );
- }
- else if( protocol.isHttp() ) {
- // FlyingSaucer will silently swallow any images that fail to load.
- // Consequently, the following lines load the resource over HTTP and
- // translate errors into a broken image icon.
- final var url = new URL( uri );
- final var image = ImageIO.read( url );
- imageResource = new ImageResource( uri, createImage( image ) );
- }
- else {
- // Caught below to return a broken image; exception is swallowed.
- throw new UnsupportedOperationException( valueOf( protocol ) );
- }
-
- return scale( imageResource );
- } catch( final Exception e ) {
- alert( e );
- return new ImageResource( uri, getBrokenImage() );
- }
- }
-
- /**
- * Scales the image found at the given URI.
- *
- * @param ir {@link ImageResource} of image loaded successfully.
- * @return Resource representing the rendered image and path.
- */
- private ImageResource scale( final ImageResource ir ) {
- final var image = ir.getImage();
- final var imageWidth = image.getWidth();
- final var imageHeight = image.getHeight();
-
- int maxWidth = mWidthProperty.get();
- int newWidth = imageWidth;
- int newHeight = imageHeight;
-
- // Maintain aspect ratio while shrinking image to view port bounds.
- if( imageWidth > maxWidth ) {
- newWidth = maxWidth;
- newHeight = (newWidth * imageHeight) / imageWidth;
- }
-
- image.scale( newWidth, newHeight );
- return ir;
- }
-
- /**
- * Lazily initializes the broken image placeholder.
- *
- * @return The {@link FSImage} that represents a broken image icon.
- */
- private FSImage getBrokenImage() {
- final var image = mBrokenImage;
-
- if( image == null ) {
- mBrokenImage = createImage( BROKEN_IMAGE_PLACEHOLDER );
- }
-
- return mBrokenImage;
- }
-}
src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preview;
-
-import com.scrivenvar.adapters.DocumentAdapter;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
-import javafx.embed.swing.SwingNode;
-import javafx.scene.Node;
-import org.jsoup.Jsoup;
-import org.jsoup.helper.W3CDom;
-import org.jsoup.nodes.Document;
-import org.xhtmlrenderer.layout.SharedContext;
-import org.xhtmlrenderer.render.Box;
-import org.xhtmlrenderer.simple.XHTMLPanel;
-import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
-import org.xhtmlrenderer.swing.*;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.event.ComponentAdapter;
-import java.awt.event.ComponentEvent;
-import java.net.URI;
-import java.nio.file.Path;
-
-import static com.scrivenvar.Constants.*;
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static com.scrivenvar.util.ProtocolResolver.getProtocol;
-import static java.awt.Desktop.Action.BROWSE;
-import static java.awt.Desktop.getDesktop;
-import static java.lang.Math.max;
-import static javax.swing.SwingUtilities.invokeLater;
-import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
-
-/**
- * HTML preview pane is responsible for rendering an HTML document.
- */
-public final class HTMLPreviewPane extends SwingNode {
-
- /**
- * Suppresses scrolling to the top on every key press.
- */
- private static class HTMLPanel extends XHTMLPanel {
- @Override
- public void resetScrollPosition() {
- }
- }
-
- /**
- * Suppresses scroll attempts until after the document has loaded.
- */
- private static final class DocumentEventHandler extends DocumentAdapter {
- private final BooleanProperty mReadyProperty = new SimpleBooleanProperty();
-
- public BooleanProperty readyProperty() {
- return mReadyProperty;
- }
-
- @Override
- public void documentStarted() {
- mReadyProperty.setValue( Boolean.FALSE );
- }
-
- @Override
- public void documentLoaded() {
- mReadyProperty.setValue( Boolean.TRUE );
- }
- }
-
- /**
- * Ensure that images are constrained to the panel width upon resizing.
- */
- private final class ResizeListener extends ComponentAdapter {
- @Override
- public void componentResized( final ComponentEvent e ) {
- setWidth( e );
- }
-
- @Override
- public void componentShown( final ComponentEvent e ) {
- setWidth( e );
- }
-
- /**
- * Sets the width of the {@link HTMLPreviewPane} so that images can be
- * scaled to fit. The scale factor is adjusted a bit below the full width
- * to prevent the horizontal scrollbar from appearing.
- *
- * @param event The component that defines the image scaling width.
- */
- private void setWidth( final ComponentEvent event ) {
- final int width = (int) (event.getComponent().getWidth() * .95);
- HTMLPreviewPane.this.mImageLoader.widthProperty().set( width );
- }
- }
-
- /**
- * Responsible for opening hyperlinks. External hyperlinks are opened in
- * the system's default browser; local file system links are opened in the
- * editor.
- */
- private static class HyperlinkListener extends LinkListener {
- @Override
- public void linkClicked( final BasicPanel panel, final String link ) {
- try {
- final var protocol = getProtocol( link );
-
- switch( protocol ) {
- case HTTP:
- final var desktop = getDesktop();
-
- if( desktop.isSupported( BROWSE ) ) {
- desktop.browse( new URI( link ) );
- }
- break;
- case FILE:
- // TODO: #88 -- publish a message to the event bus.
- break;
- }
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
- }
-
- /**
- * The CSS must be rendered in points (pt) not pixels (px) to avoid blurry
- * rendering on some platforms.
- */
- private static final String HTML_PREFIX = "<!DOCTYPE html>"
- + "<html>"
- + "<head>"
- + "<link rel='stylesheet' href='" +
- HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) + "'/>"
- + "</head>"
- + "<body>";
-
- // Provide some extra space at the end for scrolling past the last line.
- private static final String HTML_SUFFIX =
- "<p style='height=2em'>&nbsp;</p></body></html>";
-
- private static final W3CDom W3C_DOM = new W3CDom();
- private static final XhtmlNamespaceHandler NS_HANDLER =
- new XhtmlNamespaceHandler();
-
- private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
- private final int mHtmlPrefixLength;
-
- private final HTMLPanel mHtmlRenderer = new HTMLPanel();
- private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
- private final DocumentEventHandler mDocHandler = new DocumentEventHandler();
- private final CustomImageLoader mImageLoader = new CustomImageLoader();
-
- private Path mPath = DEFAULT_DIRECTORY;
-
- /**
- * Creates a new preview pane that can scroll to the caret position within the
- * document.
- */
- public HTMLPreviewPane() {
- setStyle( "-fx-background-color: white;" );
-
- // No need to append same prefix each time the HTML content is updated.
- mHtmlDocument.append( HTML_PREFIX );
- mHtmlPrefixLength = mHtmlDocument.length();
-
- // Inject an SVG renderer that produces high-quality SVG buffered images.
- final var factory = new ChainedReplacedElementFactory();
- factory.addFactory( new SvgReplacedElementFactory() );
- factory.addFactory( new SwingReplacedElementFactory(
- NO_OP_REPAINT_LISTENER, mImageLoader ) );
-
- final var context = getSharedContext();
- final var textRenderer = context.getTextRenderer();
- context.setReplacedElementFactory( factory );
- textRenderer.setSmoothingThreshold( 0 );
-
- setContent( mScrollPane );
- mHtmlRenderer.addDocumentListener( mDocHandler );
- mHtmlRenderer.addComponentListener( new ResizeListener() );
-
- // The default mouse click listener attempts navigation within the
- // preview panel. We want to usurp that behaviour to open the link in
- // a platform-specific browser.
- for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) {
- if( !(listener instanceof HoverListener) ) {
- mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener );
- }
- }
-
- mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() );
- }
-
- /**
- * Updates the internal HTML source, loads it into the preview pane, then
- * scrolls to the caret position.
- *
- * @param html The new HTML document to display.
- */
- public void process( final String html ) {
- final Document jsoupDoc = Jsoup.parse( decorate( html ) );
- final org.w3c.dom.Document w3cDoc = W3C_DOM.fromJsoup( jsoupDoc );
-
-
- // Access to a Swing component must occur from the Event Dispatch
- // thread according to Swing threading restrictions.
- invokeLater(
- () -> mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), NS_HANDLER )
- );
- }
-
- public void clear() {
- process( "" );
- }
-
- /**
- * Scrolls to an anchor link. The anchor links are injected when the
- * HTML document is created.
- *
- * @param id The unique anchor link identifier.
- */
- public void tryScrollTo( final int id ) {
- final ChangeListener<Boolean> listener = new ChangeListener<>() {
- @Override
- public void changed(
- final ObservableValue<? extends Boolean> observable,
- final Boolean oldValue,
- final Boolean newValue ) {
- if( newValue ) {
- scrollTo( id );
-
- mDocHandler.readyProperty().removeListener( this );
- }
- }
- };
-
- mDocHandler.readyProperty().addListener( listener );
- }
-
- /**
- * Scrolls to the closest element matching the given identifier without
- * waiting for the document to be ready. Be sure the document is ready
- * before calling this method.
- *
- * @param id Paragraph index.
- */
- public void scrollTo( final int id ) {
- if( id < 2 ) {
- scrollToTop();
- }
- else {
- Box box = findPrevBox( id );
- box = box == null ? findNextBox( id + 1 ) : box;
-
- if( box == null ) {
- scrollToBottom();
- }
- else {
- scrollTo( box );
- }
- }
- }
-
- private Box findPrevBox( final int id ) {
- int prevId = id;
- Box box = null;
-
- while( prevId > 0 && (box = getBoxById( PARAGRAPH_ID_PREFIX + prevId )) == null ) {
- prevId--;
- }
-
- return box;
- }
-
- private Box findNextBox( final int id ) {
- int nextId = id;
- Box box = null;
-
- while( nextId - id < 5 &&
- (box = getBoxById( PARAGRAPH_ID_PREFIX + nextId )) == null ) {
- nextId++;
- }
-
- return box;
- }
-
- private void scrollTo( final Point point ) {
- invokeLater( () -> mHtmlRenderer.scrollTo( point ) );
- }
-
- private void scrollTo( final Box box ) {
- scrollTo( createPoint( box ) );
- }
-
- private void scrollToY( final int y ) {
- scrollTo( new Point( 0, y ) );
- }
-
- private void scrollToTop() {
- scrollToY( 0 );
- }
-
- private void scrollToBottom() {
- scrollToY( mHtmlRenderer.getHeight() );
- }
-
- private Box getBoxById( final String id ) {
- return getSharedContext().getBoxById( id );
- }
-
- private String decorate( final String html ) {
- // Trim the HTML back to only the prefix.
- mHtmlDocument.setLength( mHtmlPrefixLength );
-
- // Write the HTML body element followed by closing tags.
- return mHtmlDocument.append( html ).append( HTML_SUFFIX ).toString();
- }
-
- public Path getPath() {
- return mPath;
- }
-
- public void setPath( final Path path ) {
- assert path != null;
- mPath = path;
- }
-
- /**
- * Content to embed in a panel.
- *
- * @return The content to display to the user.
- */
- public Node getNode() {
- return this;
- }
-
- public JScrollPane getScrollPane() {
- return mScrollPane;
- }
-
- public JScrollBar getVerticalScrollBar() {
- return getScrollPane().getVerticalScrollBar();
- }
-
- /**
- * Creates a {@link Point} to use as a reference for scrolling to the area
- * described by the given {@link Box}. The {@link Box} coordinates are used
- * to populate the {@link Point}'s location, with minor adjustments for
- * vertical centering.
- *
- * @param box The {@link Box} that represents a scrolling anchor reference.
- * @return A coordinate suitable for scrolling to.
- */
- private Point createPoint( final Box box ) {
- assert box != null;
-
- int x = box.getAbsX();
-
- // Scroll back up by half the height of the scroll bar to keep the typing
- // area within the view port. Otherwise the view port will have jumped too
- // high up and the whatever gets typed won't be visible.
- int y = max(
- box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2),
- 0 );
-
- if( !box.getStyle().isInline() ) {
- final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() );
- x += margin.left();
- y += margin.top();
- }
-
- return new Point( x, y );
- }
-
- private String getBaseUrl() {
- final Path basePath = getPath();
- final Path parent = basePath == null ? null : basePath.getParent();
-
- return parent == null ? "" : parent.toUri().toString();
- }
-
- private SharedContext getSharedContext() {
- return mHtmlRenderer.getSharedContext();
- }
-}
src/main/java/com/scrivenvar/preview/MathRenderer.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preview;
-
-import com.scrivenvar.preferences.UserPreferences;
-import com.whitemagicsoftware.tex.*;
-import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
-import javafx.beans.property.IntegerProperty;
-import org.w3c.dom.Document;
-
-import java.util.function.Supplier;
-
-import static com.scrivenvar.StatusBarNotifier.alert;
-
-/**
- * Responsible for rendering formulas as scalable vector graphics (SVG).
- */
-public class MathRenderer {
-
- /**
- * Default font size in points.
- */
- private static final float FONT_SIZE = 20f;
-
- private final TeXFont mTeXFont = createDefaultTeXFont( FONT_SIZE );
- private final TeXEnvironment mEnvironment = createTeXEnvironment( mTeXFont );
- private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D();
-
- public MathRenderer() {
- mGraphics.scale( FONT_SIZE, FONT_SIZE );
- }
-
- /**
- * This method only takes a few seconds to generate
- *
- * @param equation A mathematical expression to render.
- * @return The given string with all formulas transformed into SVG format.
- */
- public Document render( final String equation ) {
- final var formula = new TeXFormula( equation );
- final var box = formula.createBox( mEnvironment );
- final var l = new TeXLayout( box, FONT_SIZE );
-
- mGraphics.initialize( l.getWidth(), l.getHeight() );
- box.draw( mGraphics, l.getX(), l.getY() );
- return mGraphics.toDom();
- }
-
- @SuppressWarnings("SameParameterValue")
- private TeXFont createDefaultTeXFont( final float fontSize ) {
- return create( () -> new DefaultTeXFont( fontSize ) );
- }
-
- private TeXEnvironment createTeXEnvironment( final TeXFont texFont ) {
- return create( () -> new TeXEnvironment( texFont ) );
- }
-
- private SvgDomGraphics2D createSvgDomGraphics2D() {
- return create( SvgDomGraphics2D::new );
- }
-
- /**
- * Tries to instantiate a given object, returning {@code null} on failure.
- * The failure message is bubbled up to to the user interface.
- *
- * @param supplier Creates an instance.
- * @param <T> The type of instance being created.
- * @return An instance of the parameterized type or {@code null} upon error.
- */
- private <T> T create( final Supplier<T> supplier ) {
- try {
- return supplier.get();
- } catch( final Exception ex ) {
- alert( ex );
- return null;
- }
- }
-}
src/main/java/com/scrivenvar/preview/RenderingSettings.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preview;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import static java.awt.RenderingHints.*;
-import static java.awt.Toolkit.getDefaultToolkit;
-
-/**
- * Responsible for supplying consistent rendering hints throughout the
- * application, such as image rendering for {@link SvgRasterizer}.
- */
-@SuppressWarnings("rawtypes")
-public class RenderingSettings {
-
- /**
- * Default hints for high-quality rendering that may be changed by
- * the system's rendering hints.
- */
- private static final Map<Object, Object> DEFAULT_HINTS = Map.of(
- KEY_ANTIALIASING,
- VALUE_ANTIALIAS_ON,
- KEY_ALPHA_INTERPOLATION,
- VALUE_ALPHA_INTERPOLATION_QUALITY,
- KEY_COLOR_RENDERING,
- VALUE_COLOR_RENDER_QUALITY,
- KEY_DITHERING,
- VALUE_DITHER_DISABLE,
- KEY_FRACTIONALMETRICS,
- VALUE_FRACTIONALMETRICS_ON,
- KEY_INTERPOLATION,
- VALUE_INTERPOLATION_BICUBIC,
- KEY_RENDERING,
- VALUE_RENDER_QUALITY,
- KEY_STROKE_CONTROL,
- VALUE_STROKE_PURE,
- KEY_TEXT_ANTIALIASING,
- VALUE_TEXT_ANTIALIAS_ON
- );
-
- /**
- * Shared hints for high-quality rendering.
- */
- public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>(
- DEFAULT_HINTS
- );
-
- static {
- final var toolkit = getDefaultToolkit();
- final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" );
-
- if( hints instanceof Map ) {
- final var map = (Map) hints;
- for( final var key : map.keySet() ) {
- final var hint = map.get( key );
- RENDERING_HINTS.put( key, hint );
- }
- }
- }
-
- /**
- * Prevent instantiation as per Joshua Bloch's recommendation.
- */
- private RenderingSettings() {
- }
-}
src/main/java/com/scrivenvar/preview/SvgRasterizer.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preview;
-
-import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
-import org.apache.batik.gvt.renderer.ImageRenderer;
-import org.apache.batik.transcoder.TranscoderException;
-import org.apache.batik.transcoder.TranscoderInput;
-import org.apache.batik.transcoder.TranscoderOutput;
-import org.apache.batik.transcoder.image.ImageTranscoder;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-import java.awt.*;
-import java.awt.image.BufferedImage;
-import java.io.IOException;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.net.URL;
-import java.text.NumberFormat;
-
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static com.scrivenvar.preview.RenderingSettings.RENDERING_HINTS;
-import static java.awt.image.BufferedImage.TYPE_INT_RGB;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.text.NumberFormat.getIntegerInstance;
-import static javax.xml.transform.OutputKeys.*;
-import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
-import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
-
-/**
- * Responsible for converting SVG images into rasterized PNG images.
- */
-public class SvgRasterizer {
- private static final SAXSVGDocumentFactory FACTORY_DOM =
- new SAXSVGDocumentFactory( getXMLParserClassName() );
-
- private static final TransformerFactory FACTORY_TRANSFORM =
- TransformerFactory.newInstance();
-
- private static final Transformer sTransformer;
-
- static {
- Transformer t;
-
- try {
- t = FACTORY_TRANSFORM.newTransformer();
- t.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
- t.setOutputProperty( METHOD, "xml" );
- t.setOutputProperty( INDENT, "no" );
- t.setOutputProperty( ENCODING, UTF_8.name() );
- } catch( final TransformerConfigurationException e ) {
- t = null;
- }
-
- sTransformer = t;
- }
-
- private static final NumberFormat INT_FORMAT = getIntegerInstance();
-
- public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER;
-
- /**
- * A FontAwesome camera icon, cleft asunder.
- */
- public static final String BROKEN_IMAGE_SVG =
- "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" +
- ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" +
- ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " +
- "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" +
- ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" +
- ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" +
- ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" +
- ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " +
- "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" +
- ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" +
- ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" +
- ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" +
- ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" +
- ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" +
- ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" +
- ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" +
- ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " +
- ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" +
- ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" +
- ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" +
- ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" +
- ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" +
- ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" +
- ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " +
- "0'/></g></svg>";
-
- static {
- // The width and height cannot be embedded in the SVG above because the
- // path element values are relative to the viewBox dimensions.
- final int w = 75;
- final int h = 75;
- BufferedImage image;
-
- try {
- image = rasterizeString( BROKEN_IMAGE_SVG, w );
- } catch( final Exception e ) {
- image = new BufferedImage( w, h, TYPE_INT_RGB );
- final var graphics = (Graphics2D) image.getGraphics();
- graphics.setRenderingHints( RENDERING_HINTS );
-
- // Fall back to a (\) symbol.
- graphics.setColor( new Color( 204, 204, 204 ) );
- graphics.fillRect( 0, 0, w, h );
- graphics.setColor( new Color( 255, 204, 204 ) );
- graphics.setStroke( new BasicStroke( 4 ) );
- graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
- graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
- h / 4 + (int) (w / 4 / Math.PI),
- w / 2 + w / 4 - (int) (w / 4 / Math.PI),
- h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
- }
-
- BROKEN_IMAGE_PLACEHOLDER = image;
- }
-
- /**
- * Responsible for creating a new {@link ImageRenderer} implementation that
- * can render a DOM as an SVG image.
- */
- private static class BufferedImageTranscoder extends ImageTranscoder {
- private BufferedImage mImage;
-
- @Override
- public BufferedImage createImage( final int w, final int h ) {
- return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
- }
-
- @Override
- public void writeImage(
- final BufferedImage image, final TranscoderOutput output ) {
- mImage = image;
- }
-
- public BufferedImage getImage() {
- return mImage;
- }
-
- @Override
- protected ImageRenderer createRenderer() {
- final ImageRenderer renderer = super.createRenderer();
- final RenderingHints hints = renderer.getRenderingHints();
- hints.putAll( RENDERING_HINTS );
-
- renderer.setRenderingHints( hints );
-
- return renderer;
- }
- }
-
- /**
- * Rasterizes the vector graphic file at the given URL. If any exception
- * happens, a red circle is returned instead.
- *
- * @param url The URL to a vector graphic file, which must include the
- * protocol scheme (such as file:// or https://).
- * @param width The number of pixels wide to render the image. The aspect
- * ratio is maintained.
- * @return Either the rasterized image upon success or a red circle.
- */
- public static BufferedImage rasterize( final String url, final int width ) {
- try {
- return rasterize( new URL( url ), width );
- } catch( final Exception ex ) {
- alert( ex );
- return BROKEN_IMAGE_PLACEHOLDER;
- }
- }
-
- /**
- * Rasterizes the given document into an image.
- *
- * @param svg The SVG {@link Document} to rasterize.
- * @param width The rasterized image's width (in pixels).
- * @return The rasterized image.
- * @throws TranscoderException Signifies an issue with the input document.
- */
- public static BufferedImage rasterize( final Document svg, final int width )
- throws TranscoderException {
- final var transcoder = new BufferedImageTranscoder();
- final var input = new TranscoderInput( svg );
-
- transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
- transcoder.transcode( input, null );
-
- return transcoder.getImage();
- }
-
- /**
- * Converts an SVG drawing into a rasterized image that can be drawn on
- * a graphics context.
- *
- * @param url The path to the image (can be web address).
- * @param width Scale the image width to this size (aspect ratio is
- * maintained).
- * @return The vector graphic transcoded into a raster image format.
- * @throws IOException Could not read the vector graphic.
- * @throws TranscoderException Could not convert the vector graphic to an
- * instance of {@link Image}.
- */
- public static BufferedImage rasterize( final URL url, final int width )
- throws IOException, TranscoderException {
- return rasterize( FACTORY_DOM.createDocument( url.toString() ), width );
- }
-
- public static BufferedImage rasterize( final Document document ) {
- try {
- final var root = document.getDocumentElement();
- final var width = root.getAttribute( "width" );
- return rasterize( document, INT_FORMAT.parse( width ).intValue() );
- } catch( final Exception ex ) {
- alert( ex );
- return BROKEN_IMAGE_PLACEHOLDER;
- }
- }
-
- /**
- * Converts an SVG string into a rasterized image that can be drawn on
- * a graphics context.
- *
- * @param svg The SVG xml document.
- * @param w Scale the image width to this size (aspect ratio is
- * maintained).
- * @return The vector graphic transcoded into a raster image format.
- * @throws TranscoderException Could not convert the vector graphic to an
- * instance of {@link Image}.
- */
- public static BufferedImage rasterizeString( final String svg, final int w )
- throws IOException, TranscoderException {
- return rasterize( toDocument( svg ), w );
- }
-
- /**
- * Converts an SVG string into a rasterized image that can be drawn on
- * a graphics context. The dimensions are determined from the document.
- *
- * @param xml The SVG xml document.
- * @return The vector graphic transcoded into a raster image format.
- */
- public static BufferedImage rasterizeString( final String xml ) {
- try {
- final var document = toDocument( xml );
- final var root = document.getDocumentElement();
- final var width = root.getAttribute( "width" );
- return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() );
- } catch( final Exception ex ) {
- alert( ex );
- return BROKEN_IMAGE_PLACEHOLDER;
- }
- }
-
- /**
- * Converts an SVG XML string into a new {@link Document} instance.
- *
- * @param xml The XML containing SVG elements.
- * @return The SVG contents parsed into a {@link Document} object model.
- * @throws IOException Could
- */
- private static Document toDocument( final String xml ) throws IOException {
- try( final var reader = new StringReader( xml ) ) {
- return FACTORY_DOM.createSVGDocument(
- "http://www.w3.org/2000/svg", reader );
- }
- }
-
- /**
- * Given a document object model (DOM) {@link Element}, this will convert that
- * element to a string.
- *
- * @param e The DOM node to convert to a string.
- * @return The DOM node as an escaped, plain text string.
- */
- public static String toSvg( final Element e ) {
- try( final var writer = new StringWriter() ) {
- sTransformer.transform( new DOMSource( e ), new StreamResult( writer ) );
- return writer.toString().replaceAll( "xmlns=\"\" ", "" );
- } catch( final Exception ex ) {
- alert( ex );
- }
-
- return BROKEN_IMAGE_SVG;
- }
-}
src/main/java/com/scrivenvar/preview/SvgReplacedElementFactory.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.preview;
-
-import com.scrivenvar.util.BoundedCache;
-import org.apache.commons.io.FilenameUtils;
-import org.w3c.dom.Element;
-import org.xhtmlrenderer.extend.ReplacedElement;
-import org.xhtmlrenderer.extend.ReplacedElementFactory;
-import org.xhtmlrenderer.extend.UserAgentCallback;
-import org.xhtmlrenderer.layout.LayoutContext;
-import org.xhtmlrenderer.render.BlockBox;
-import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
-import org.xhtmlrenderer.swing.ImageReplacedElement;
-
-import java.awt.image.BufferedImage;
-import java.util.Map;
-import java.util.function.Function;
-
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static com.scrivenvar.preview.SvgRasterizer.rasterize;
-import static com.scrivenvar.processors.markdown.tex.TeXNode.HTML_TEX;
-
-/**
- * Responsible for running {@link SvgRasterizer} on SVG images detected within
- * a document to transform them into rasterized versions.
- */
-public class SvgReplacedElementFactory implements ReplacedElementFactory {
-
- /**
- * Prevent instantiation until needed.
- */
- private static class MathRendererContainer {
- private static final MathRenderer INSTANCE = new MathRenderer();
- }
-
- /**
- * Returns the singleton instance for rendering math symbols.
- *
- * @return A non-null instance, loaded, configured, and ready to render math.
- */
- public static MathRenderer getInstance() {
- return MathRendererContainer.INSTANCE;
- }
-
- /**
- * SVG filename extension maps to an SVG image element.
- */
- private static final String SVG_FILE = "svg";
-
- private static final String HTML_IMAGE = "img";
- private static final String HTML_IMAGE_SRC = "src";
-
- /**
- * A bounded cache that removes the oldest image if the maximum number of
- * cached images has been reached. This constrains the number of images
- * loaded into memory.
- */
- private final Map<String, BufferedImage> mImageCache =
- new BoundedCache<>( 150 );
-
- @Override
- public ReplacedElement createReplacedElement(
- final LayoutContext c,
- final BlockBox box,
- final UserAgentCallback uac,
- final int cssWidth,
- final int cssHeight ) {
- BufferedImage image = null;
- final var e = box.getElement();
-
- if( e != null ) {
- try {
- final var nodeName = e.getNodeName();
-
- if( HTML_IMAGE.equals( nodeName ) ) {
- final var src = e.getAttribute( HTML_IMAGE_SRC );
- final var ext = FilenameUtils.getExtension( src );
-
- if( SVG_FILE.equalsIgnoreCase( ext ) ) {
- image = getCachedImage(
- src, svg -> rasterize( svg, box.getContentWidth() ) );
- }
- }
- else if( HTML_TEX.equals( nodeName ) ) {
- // Convert the TeX element to a raster graphic if not yet cached.
- final var src = e.getTextContent();
- image = getCachedImage(
- src, __ -> rasterize( getInstance().render( src ) )
- );
- }
- } catch( final Exception ex ) {
- alert( ex );
- }
- }
-
- if( image != null ) {
- final var w = image.getWidth( null );
- final var h = image.getHeight( null );
-
- return new ImageReplacedElement( image, w, h );
- }
-
- return null;
- }
-
- @Override
- public void reset() {
- }
-
- @Override
- public void remove( final Element e ) {
- }
-
- @Override
- public void setFormSubmissionListener( FormSubmissionListener listener ) {
- }
-
- /**
- * Returns an image associated with a string; the string's pre-computed
- * hash code is returned as the string value, making this operation very
- * quick to return the corresponding {@link BufferedImage}.
- *
- * @param src The source used for the key into the image cache.
- * @param rasterizer {@link Function} to call to rasterize an image.
- * @return The image that corresponds to the given source string.
- */
- private BufferedImage getCachedImage(
- final String src, final Function<String, BufferedImage> rasterizer ) {
- return mImageCache.computeIfAbsent( src, __ -> rasterizer.apply( src ) );
- }
-}
src/main/java/com/scrivenvar/processors/AbstractProcessor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-/**
- * Responsible for transforming a document through a variety of chained
- * handlers. If there are conditions where this handler should not process the
- * entire chain, create a second handler, or split the chain into reusable
- * sub-chains.
- *
- * @param <T> The type of object to process.
- */
-public abstract class AbstractProcessor<T> implements Processor<T> {
-
- /**
- * Used while processing the entire chain; null to signify no more links.
- */
- private final Processor<T> mNext;
-
- /**
- * Constructs a new default handler with no successor.
- */
- protected AbstractProcessor() {
- this( null );
- }
-
- /**
- * Constructs a new default handler with a given successor.
- *
- * @param successor The next processor in the chain.
- */
- public AbstractProcessor( final Processor<T> successor ) {
- mNext = successor;
- }
-
- @Override
- public Processor<T> next() {
- return mNext;
- }
-
- /**
- * This algorithm is incorrect, but works for the one use case of removing
- * the ending HTML Preview Processor from the end of the processor chain.
- * The processor chain is immutable so this creates a succession of
- * delegators that wrap each processor in the chain, except for the one
- * to be removed.
- * <p>
- * An alternative is to update the {@link ProcessorFactory} with the ability
- * to create a processor chain devoid of an {@link HtmlPreviewProcessor}.
- * </p>
- *
- * @param removal The {@link Processor} to remove from the chain.
- * @return A delegating processor chain starting from this processor
- * onwards with the given processor removed from the chain.
- */
- @Override
- public Processor<T> remove( final Class<? extends Processor<T>> removal ) {
- Processor<T> p = this;
- final ProcessorDelegator<T> head = new ProcessorDelegator<>( p );
- ProcessorDelegator<T> result = head;
-
- while( p != null ) {
- final Processor<T> next = p.next();
-
- if( next != null && next.getClass() != removal ) {
- final var delegator = new ProcessorDelegator<>( next );
-
- result.setNext( delegator );
- result = delegator;
- }
-
- p = p.next();
- }
-
- return head;
- }
-
- private static final class ProcessorDelegator<T>
- extends AbstractProcessor<T> {
- private final Processor<T> mDelegate;
- private Processor<T> mNext;
-
- public ProcessorDelegator( final Processor<T> delegate ) {
- super( delegate );
-
- assert delegate != null;
-
- mDelegate = delegate;
- }
-
- @Override
- public T apply( T t ) {
- return mDelegate.apply( t );
- }
-
- protected void setNext( final Processor<T> next ) {
- mNext = next;
- }
-
- @Override
- public Processor<T> next() {
- return mNext;
- }
- }
-}
src/main/java/com/scrivenvar/processors/DefinitionProcessor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-import java.util.Map;
-
-import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
-
-/**
- * Processes interpolated string definitions in the document and inserts
- * their values into the post-processed text. The default variable syntax is
- * {@code $variable$}.
- */
-public class DefinitionProcessor extends AbstractProcessor<String> {
-
- private final Map<String, String> mDefinitions;
-
- public DefinitionProcessor(
- final Processor<String> successor, final Map<String, String> map ) {
- super( successor );
- mDefinitions = map;
- }
-
- /**
- * Processes the given text document by replacing variables with their values.
- *
- * @param text The document text that includes variables that should be
- * replaced with values when rendered as HTML.
- * @return The text with all variables replaced.
- */
- @Override
- public String apply( final String text ) {
- return replace( text, getDefinitions() );
- }
-
- /**
- * Returns the map to use for variable substitution.
- *
- * @return A map of variable names to values.
- */
- protected Map<String, String> getDefinitions() {
- return mDefinitions;
- }
-}
src/main/java/com/scrivenvar/processors/HtmlPreviewProcessor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-import com.scrivenvar.preview.HTMLPreviewPane;
-
-/**
- * Responsible for notifying the HTMLPreviewPane when the succession chain has
- * updated. This decouples knowledge of changes to the editor panel from the
- * HTML preview panel as well as any processing that takes place before the
- * final HTML preview is rendered. This should be the last link in the processor
- * chain.
- */
-public class HtmlPreviewProcessor extends AbstractProcessor<String> {
-
- // There is only one preview panel.
- private static HTMLPreviewPane sHtmlPreviewPane;
-
- /**
- * Constructs the end of a processing chain.
- *
- * @param htmlPreviewPane The pane to update with the post-processed document.
- */
- public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
- sHtmlPreviewPane = htmlPreviewPane;
- }
-
- /**
- * Update the preview panel using HTML from the succession chain.
- *
- * @param html The document content to render in the preview pane. The HTML
- * should not contain a doctype, head, or body tag, only
- * content to render within the body.
- * @return {@code null} to indicate no more processors in the chain.
- */
- @Override
- public String apply( final String html ) {
- getHtmlPreviewPane().process( html );
-
- // No more processing required.
- return null;
- }
-
- private HTMLPreviewPane getHtmlPreviewPane() {
- return sHtmlPreviewPane;
- }
-}
src/main/java/com/scrivenvar/processors/IdentityProcessor.java
-/*
- * Copyright 2017 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-/**
- * This is the default processor used when an unknown filename extension is
- * encountered.
- */
-public class IdentityProcessor extends AbstractProcessor<String> {
-
- /**
- * Passes the link to the super constructor.
- *
- * @param successor The next processor in the chain to use for text
- * processing.
- */
- public IdentityProcessor( final Processor<String> successor ) {
- super( successor );
- }
-
- /**
- * Returns the given string, modified with "pre" tags.
- *
- * @param t The string to return, enclosed in "pre" tags.
- * @return The value of t wrapped in "pre" tags.
- */
- @Override
- public String apply( final String t ) {
- return "<pre>" + t + "</pre>";
- }
-}
src/main/java/com/scrivenvar/processors/InlineRProcessor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-import com.scrivenvar.preferences.UserPreferences;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.StringProperty;
-
-import javax.script.ScriptEngine;
-import javax.script.ScriptEngineManager;
-import java.io.File;
-import java.nio.file.Path;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import static com.scrivenvar.Constants.STATUS_PARSE_ERROR;
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
-import static com.scrivenvar.sigils.RSigilOperator.PREFIX;
-import static com.scrivenvar.sigils.RSigilOperator.SUFFIX;
-import static java.lang.Math.min;
-
-/**
- * Transforms a document containing R statements into Markdown.
- */
-public final class InlineRProcessor extends DefinitionProcessor {
- /**
- * Constrain memory when typing new R expressions into the document.
- */
- private static final int MAX_CACHED_R_STATEMENTS = 512;
-
- /**
- * Where to put document inline evaluated R expressions.
- */
- private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
- @Override
- protected boolean removeEldestEntry(
- final Map.Entry<String, Object> eldest ) {
- return size() > MAX_CACHED_R_STATEMENTS;
- }
- };
-
- /**
- * Only one editor is open at a time.
- */
- private static final ScriptEngine ENGINE =
- (new ScriptEngineManager()).getEngineByName( "Renjin" );
-
- private static final int PREFIX_LENGTH = PREFIX.length();
-
- private final AtomicBoolean mDirty = new AtomicBoolean( false );
-
- /**
- * Constructs a processor capable of evaluating R statements.
- *
- * @param successor Subsequent link in the processing chain.
- * @param map Resolved definitions map.
- */
- public InlineRProcessor(
- final Processor<String> successor,
- final Map<String, String> map ) {
- super( successor, map );
-
- bootstrapScriptProperty().addListener(
- ( ob, oldScript, newScript ) -> setDirty( true ) );
- workingDirectoryProperty().addListener(
- ( ob, oldScript, newScript ) -> setDirty( true ) );
-
- getUserPreferences().addSaveEventHandler( ( handler ) -> {
- if( isDirty() ) {
- init();
- setDirty( false );
- }
- } );
-
- init();
- }
-
- /**
- * Initialises the R code so that R can find imported libraries. Note that
- * any existing R functionality will not be overwritten if this method is
- * called multiple times.
- */
- private void init() {
- final var bootstrap = getBootstrapScript();
-
- if( !bootstrap.isBlank() ) {
- final var wd = getWorkingDirectory();
- final var dir = wd.toString().replace( '\\', '/' );
- final var map = getDefinitions();
- map.put( "$application.r.working.directory$", dir );
-
- eval( replace( bootstrap, map ) );
- }
- }
-
- /**
- * Sets the dirty flag to indicate that the bootstrap script or working
- * directory has been modified. Upon saving the preferences, if this flag
- * is true, then {@link #init()} will be called to reload the R environment.
- *
- * @param dirty Set to true to reload changes upon closing preferences.
- */
- private void setDirty( final boolean dirty ) {
- mDirty.set( dirty );
- }
-
- /**
- * Answers whether R-related settings have been modified.
- *
- * @return {@code true} when the settings have changed.
- */
- private boolean isDirty() {
- return mDirty.get();
- }
-
- /**
- * Evaluates all R statements in the source document and inserts the
- * calculated value into the generated document.
- *
- * @param text The document text that includes variables that should be
- * replaced with values when rendered as HTML.
- * @return The generated document with output from all R statements
- * substituted with value returned from their execution.
- */
- @Override
- public String apply( final String text ) {
- final int length = text.length();
-
- // The * 2 is a wild guess at the ratio of R statements to the length
- // of text produced by those statements.
- final StringBuilder sb = new StringBuilder( length * 2 );
-
- int prevIndex = 0;
- int currIndex = text.indexOf( PREFIX );
-
- while( currIndex >= 0 ) {
- // Copy everything up to, but not including, an R statement (`r#).
- sb.append( text, prevIndex, currIndex );
-
- // Jump to the start of the R statement.
- prevIndex = currIndex + PREFIX_LENGTH;
-
- // Find the statement ending (`), without indexing past the text boundary.
- currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
-
- // Only evaluate inline R statements that have end delimiters.
- if( currIndex > 1 ) {
- // Extract the inline R statement to be evaluated.
- final String r = text.substring( prevIndex, currIndex );
-
- // Pass the R statement into the R engine for evaluation.
- try {
- final Object result = evalText( r );
-
- // Append the string representation of the result into the text.
- sb.append( result );
- } catch( final Exception e ) {
- // If the string couldn't be parsed using R, append the statement
- // that failed to parse, instead of its evaluated value.
- sb.append( PREFIX ).append( r ).append( SUFFIX );
-
- // Tell the user that there was a problem.
- alert( STATUS_PARSE_ERROR, e.getMessage(), currIndex );
- }
-
- // Retain the R statement's ending position in the text.
- prevIndex = currIndex + 1;
- }
-
- // Find the start of the next inline R statement.
- currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
- }
-
- // Copy from the previous index to the end of the string.
- return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
- }
-
- /**
- * Look up an R expression from the cache then return the resulting object.
- * If the R expression hasn't been cached, it'll first be evaluated.
- *
- * @param r The expression to evaluate.
- * @return The object resulting from the evaluation.
- */
- private Object evalText( final String r ) {
- return mEvalCache.computeIfAbsent( r, v -> eval( r ) );
- }
-
- /**
- * Evaluate an R expression and return the resulting object.
- *
- * @param r The expression to evaluate.
- * @return The object resulting from the evaluation.
- */
- private Object eval( final String r ) {
- try {
- return getScriptEngine().eval( r );
- } catch( final Exception ex ) {
- final String expr = r.substring( 0, min( r.length(), 30 ) );
- alert( "Main.status.error.r", expr, ex.getMessage() );
- }
-
- return "";
- }
-
- /**
- * Return the given path if not {@code null}, otherwise return the path to
- * the user's directory.
- *
- * @return A non-null path.
- */
- private Path getWorkingDirectory() {
- return getUserPreferences().getRDirectory().toPath();
- }
-
- private ObjectProperty<File> workingDirectoryProperty() {
- return getUserPreferences().rDirectoryProperty();
- }
-
- /**
- * Loads the R init script from the application's persisted preferences.
- *
- * @return A non-null string, possibly empty.
- */
- private String getBootstrapScript() {
- return getUserPreferences().getRScript();
- }
-
- private StringProperty bootstrapScriptProperty() {
- return getUserPreferences().rScriptProperty();
- }
-
- private UserPreferences getUserPreferences() {
- return UserPreferences.getInstance();
- }
-
- private ScriptEngine getScriptEngine() {
- return ENGINE;
- }
-}
src/main/java/com/scrivenvar/processors/Processor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-import java.util.function.UnaryOperator;
-
-/**
- * Responsible for processing documents from one known format to another.
- * Processes the given content providing a transformation from one document
- * format into another. For example, this could convert from XML to text using
- * an XSLT processor, or from markdown to HTML.
- *
- * @param <T> The type of processor to create.
- */
-public interface Processor<T> extends UnaryOperator<T> {
-
- /**
- * Removes the given processor from the chain, returning a new immutable
- * chain equivalent to this chain, but without the given processor.
- *
- * @param processor The {@link Processor} to remove from the chain.
- * @return A delegating processor chain starting from this processor
- * onwards with the given processor removed from the chain.
- */
- Processor<T> remove( Class<? extends Processor<T>> processor );
-
- /**
- * Adds a document processor to call after this processor finishes processing
- * the document given to the process method.
- *
- * @return The processor that should transform the document after this
- * instance has finished processing, or {@code null} if this is the last
- * processor in the chain.
- */
- default Processor<T> next() {
- return null;
- }
-}
src/main/java/com/scrivenvar/processors/ProcessorFactory.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-import com.scrivenvar.AbstractFileFactory;
-import com.scrivenvar.FileEditorTab;
-import com.scrivenvar.preview.HTMLPreviewPane;
-import com.scrivenvar.processors.markdown.MarkdownProcessor;
-
-import java.util.Map;
-
-/**
- * Responsible for creating processors capable of parsing, transforming,
- * interpolating, and rendering known file types.
- */
-public class ProcessorFactory extends AbstractFileFactory {
-
- private final HTMLPreviewPane mPreviewPane;
- private final Map<String, String> mResolvedMap;
- private final Processor<String> mMarkdownProcessor;
-
- /**
- * Constructs a factory with the ability to create processors that can perform
- * text and caret processing to generate a final preview.
- *
- * @param previewPane Where the final output is rendered.
- * @param resolvedMap Flat map of definitions to replace before final render.
- */
- public ProcessorFactory(
- final HTMLPreviewPane previewPane,
- final Map<String, String> resolvedMap ) {
- mPreviewPane = previewPane;
- mResolvedMap = resolvedMap;
- mMarkdownProcessor = createMarkdownProcessor();
- }
-
- /**
- * Creates a processor chain suitable for parsing and rendering the file
- * opened at the given tab.
- *
- * @param tab The tab containing a text editor, path, and caret position.
- * @return A processor that can render the given tab's text.
- */
- public Processor<String> createProcessors( final FileEditorTab tab ) {
- return switch( lookup( tab.getPath() ) ) {
- case RMARKDOWN -> createRProcessor();
- case SOURCE -> createMarkdownDefinitionProcessor();
- case XML -> createXMLProcessor( tab );
- case RXML -> createRXMLProcessor( tab );
- default -> createIdentityProcessor();
- };
- }
-
- private Processor<String> createHTMLPreviewProcessor() {
- return new HtmlPreviewProcessor( getPreviewPane() );
- }
-
- /**
- * Creates and links the processors at the end of the processing chain.
- *
- * @return A markdown, caret replacement, and preview pane processor chain.
- */
- private Processor<String> createMarkdownProcessor() {
- final var hpp = createHTMLPreviewProcessor();
- return new MarkdownProcessor( hpp, getPreviewPane().getPath() );
- }
-
- protected Processor<String> createIdentityProcessor() {
- final var hpp = createHTMLPreviewProcessor();
- return new IdentityProcessor( hpp );
- }
-
- protected Processor<String> createDefinitionProcessor(
- final Processor<String> p ) {
- return new DefinitionProcessor( p, getResolvedMap() );
- }
-
- protected Processor<String> createMarkdownDefinitionProcessor() {
- final var tpc = getCommonProcessor();
- return createDefinitionProcessor( tpc );
- }
-
- protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
- final var tpc = getCommonProcessor();
- final var xmlp = new XmlProcessor( tpc, tab.getPath() );
- return createDefinitionProcessor( xmlp );
- }
-
- protected Processor<String> createRProcessor() {
- final var tpc = getCommonProcessor();
- final var rp = new InlineRProcessor( tpc, getResolvedMap() );
- return new RVariableProcessor( rp, getResolvedMap() );
- }
-
- protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
- final var tpc = getCommonProcessor();
- final var xmlp = new XmlProcessor( tpc, tab.getPath() );
- final var rp = new InlineRProcessor( xmlp, getResolvedMap() );
- return new RVariableProcessor( rp, getResolvedMap() );
- }
-
- private HTMLPreviewPane getPreviewPane() {
- return mPreviewPane;
- }
-
- /**
- * Returns the variable map of interpolated definitions.
- *
- * @return A map to help dereference variables.
- */
- private Map<String, String> getResolvedMap() {
- return mResolvedMap;
- }
-
- /**
- * Returns a processor common to all processors: markdown, caret position
- * token replacer, and an HTML preview renderer.
- *
- * @return Processors at the end of the processing chain.
- */
- private Processor<String> getCommonProcessor() {
- return mMarkdownProcessor;
- }
-}
src/main/java/com/scrivenvar/processors/RVariableProcessor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-import com.scrivenvar.sigils.RSigilOperator;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Converts the keys of the resolved map from default form to R form, then
- * performs a substitution on the text. The default R variable syntax is
- * {@code v$tree$leaf}.
- */
-public class RVariableProcessor extends DefinitionProcessor {
-
- public RVariableProcessor(
- final Processor<String> rp, final Map<String, String> map ) {
- super( rp, map );
- }
-
- /**
- * Returns the R-based version of the interpolated variable definitions.
- *
- * @return Variable names transmogrified from the default syntax to R syntax.
- */
- @Override
- protected Map<String, String> getDefinitions() {
- return toR( super.getDefinitions() );
- }
-
- /**
- * Converts the given map from regular variables to R variables.
- *
- * @param map Map of variable names to values.
- * @return Map of R variables.
- */
- private Map<String, String> toR( final Map<String, String> map ) {
- final var rMap = new HashMap<String, String>( map.size() );
-
- for( final var entry : map.entrySet() ) {
- final var key = entry.getKey();
- rMap.put( RSigilOperator.entoken( key ), toRValue( map.get( key ) ) );
- }
-
- return rMap;
- }
-
- private String toRValue( final String value ) {
- return '\'' + escape( value, '\'', "\\'" ) + '\'';
- }
-
- /**
- * TODO: Make generic method for replacing text.
- *
- * @param haystack Search this string for the needle, must not be null.
- * @param needle The character to find in the haystack.
- * @param thread Replace the needle with this text, if the needle is found.
- * @return The haystack with the all instances of needle replaced with thread.
- */
- @SuppressWarnings("SameParameterValue")
- private String escape(
- final String haystack, final char needle, final String thread ) {
- int end = haystack.indexOf( needle );
-
- if( end < 0 ) {
- return haystack;
- }
-
- final int length = haystack.length();
- int start = 0;
-
- // Replace up to 32 occurrences before the string reallocates its buffer.
- final StringBuilder sb = new StringBuilder( length + 32 );
-
- while( end >= 0 ) {
- sb.append( haystack, start, end ).append( thread );
- start = end + 1;
- end = haystack.indexOf( needle, start );
- }
-
- return sb.append( haystack.substring( start ) ).toString();
- }
-}
src/main/java/com/scrivenvar/processors/XmlProcessor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors;
-
-import com.scrivenvar.Services;
-import com.scrivenvar.service.Snitch;
-import net.sf.saxon.TransformerFactoryImpl;
-import net.sf.saxon.trans.XPathException;
-
-import javax.xml.stream.XMLEventReader;
-import javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.events.ProcessingInstruction;
-import javax.xml.stream.events.XMLEvent;
-import javax.xml.transform.*;
-import javax.xml.transform.stream.StreamResult;
-import javax.xml.transform.stream.StreamSource;
-import java.io.File;
-import java.io.Reader;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
-
-/**
- * Transforms an XML document. The XML document must have a stylesheet specified
- * as part of its processing instructions, such as:
- * <p>
- * {@code xml-stylesheet type="text/xsl" href="markdown.xsl"}
- * </p>
- * <p>
- * The XSL must transform the XML document into Markdown, or another format
- * recognized by the next link on the chain.
- * </p>
- */
-public class XmlProcessor extends AbstractProcessor<String>
- implements ErrorListener {
-
- private final Snitch snitch = Services.load( Snitch.class );
-
- private XMLInputFactory xmlInputFactory;
- private TransformerFactory transformerFactory;
- private Transformer transformer;
-
- private Path path;
-
- /**
- * Constructs an XML processor that can transform an XML document into another
- * format based on the XSL file specified as a processing instruction. The
- * path must point to the directory where the XSL file is found, which implies
- * that they must be in the same directory.
- *
- * @param processor Next link in the processing chain.
- * @param path The path to the XML file content to be processed.
- */
- public XmlProcessor( final Processor<String> processor, final Path path ) {
- super( processor );
- setPath( path );
- }
-
- /**
- * Transforms the given XML text into another form (typically Markdown).
- *
- * @param text The text to transform, can be empty, cannot be null.
- * @return The transformed text, or empty if text is empty.
- */
- @Override
- public String apply( final String text ) {
- try {
- return text.isEmpty() ? text : transform( text );
- } catch( final Exception ex ) {
- throw new RuntimeException( ex );
- }
- }
-
- /**
- * Performs an XSL transformation on the given XML text. The XML text must
- * have a processing instruction that points to the XSL template file to use
- * for the transformation.
- *
- * @param text The text to transform.
- * @return The transformed text.
- */
- private String transform( final String text ) throws Exception {
- // Extract the XML stylesheet processing instruction.
- final String template = getXsltFilename( text );
- final Path xsl = getXslPath( template );
-
- try(
- final StringWriter output = new StringWriter( text.length() );
- final StringReader input = new StringReader( text ) ) {
-
- // Listen for external file modification events.
- getSnitch().listen( xsl );
-
- getTransformer( xsl ).transform(
- new StreamSource( input ),
- new StreamResult( output )
- );
-
- return output.toString();
- }
- }
-
- /**
- * Returns an XSL transformer ready to transform an XML document using the
- * XSLT file specified by the given path. If the path is already known then
- * this will return the associated transformer.
- *
- * @param xsl The path to an XSLT file.
- * @return A transformer that will transform XML documents using the given
- * XSLT file.
- * @throws TransformerConfigurationException Could not instantiate the
- * transformer.
- */
- private Transformer getTransformer( final Path xsl )
- throws TransformerConfigurationException {
- if( this.transformer == null ) {
- this.transformer = createTransformer( xsl );
- }
-
- return this.transformer;
- }
-
- /**
- * Creates a configured transformer ready to run.
- *
- * @param xsl The stylesheet to use for transforming XML documents.
- * @return The edited XML document transformed into another format (usually
- * markdown).
- * @throws TransformerConfigurationException Could not create the transformer.
- */
- protected Transformer createTransformer( final Path xsl )
- throws TransformerConfigurationException {
- final Source xslt = new StreamSource( xsl.toFile() );
-
- return getTransformerFactory().newTransformer( xslt );
- }
-
- private Path getXslPath( final String filename ) {
- final Path xmlPath = getPath();
- final File xmlDirectory = xmlPath.toFile().getParentFile();
-
- return Paths.get( xmlDirectory.getPath(), filename );
- }
-
- /**
- * Given XML text, this will use a StAX pull reader to obtain the XML
- * stylesheet processing instruction. This will throw a parse exception if the
- * href pseudo-attribute filename value cannot be found.
- *
- * @param xml The XML containing an xml-stylesheet processing instruction.
- * @return The href pseudo-attribute value.
- * @throws XMLStreamException Could not parse the XML file.
- */
- private String getXsltFilename( final String xml )
- throws XMLStreamException, XPathException {
-
- String result = "";
-
- try( final StringReader sr = new StringReader( xml ) ) {
- boolean found = false;
- int count = 0;
- final XMLEventReader reader = createXMLEventReader( sr );
-
- // If the processing instruction wasn't found in the first 10 lines,
- // fail fast. This should iterate twice through the loop.
- while( !found && reader.hasNext() && count++ < 10 ) {
- final XMLEvent event = reader.nextEvent();
-
- if( event.isProcessingInstruction() ) {
- final ProcessingInstruction pi = (ProcessingInstruction) event;
- final String target = pi.getTarget();
-
- if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
- result = getPseudoAttribute( pi.getData(), "href" );
- found = true;
- }
- }
- }
- }
-
- return result;
- }
-
- private XMLEventReader createXMLEventReader( final Reader reader )
- throws XMLStreamException {
- return getXMLInputFactory().createXMLEventReader( reader );
- }
-
- private synchronized XMLInputFactory getXMLInputFactory() {
- if( this.xmlInputFactory == null ) {
- this.xmlInputFactory = createXMLInputFactory();
- }
-
- return this.xmlInputFactory;
- }
-
- private XMLInputFactory createXMLInputFactory() {
- return XMLInputFactory.newInstance();
- }
-
- private synchronized TransformerFactory getTransformerFactory() {
- if( this.transformerFactory == null ) {
- this.transformerFactory = createTransformerFactory();
- }
-
- return this.transformerFactory;
- }
-
- /**
- * Returns a high-performance XSLT 2 transformation engine.
- *
- * @return An XSL transforming engine.
- */
- private TransformerFactory createTransformerFactory() {
- final TransformerFactory factory = new TransformerFactoryImpl();
-
- // Bubble problems up to the user interface, rather than standard error.
- factory.setErrorListener( this );
-
- return factory;
- }
-
- /**
- * Called when the XSL transformer issues a warning.
- *
- * @param ex The problem the transformer encountered.
- */
- @Override
- public void warning( final TransformerException ex ) {
- throw new RuntimeException( ex );
- }
-
- /**
- * Called when the XSL transformer issues an error.
- *
- * @param ex The problem the transformer encountered.
- */
- @Override
- public void error( final TransformerException ex ) {
- throw new RuntimeException( ex );
- }
-
- /**
- * Called when the XSL transformer issues a fatal error, which is probably
- * a bit over-dramatic a method name.
- *
- * @param ex The problem the transformer encountered.
- */
- @Override
- public void fatalError( final TransformerException ex ) {
- throw new RuntimeException( ex );
- }
-
- private void setPath( final Path path ) {
- this.path = path;
- }
-
- private Path getPath() {
- return this.path;
- }
-
- private Snitch getSnitch() {
- return this.snitch;
- }
-}
src/main/java/com/scrivenvar/processors/markdown/BlockExtension.java
-package com.scrivenvar.processors.markdown;
-
-import com.vladsch.flexmark.ast.BlockQuote;
-import com.vladsch.flexmark.ast.ListBlock;
-import com.vladsch.flexmark.html.AttributeProvider;
-import com.vladsch.flexmark.html.AttributeProviderFactory;
-import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
-import com.vladsch.flexmark.html.renderer.AttributablePart;
-import com.vladsch.flexmark.html.renderer.LinkResolverContext;
-import com.vladsch.flexmark.util.ast.Block;
-import com.vladsch.flexmark.util.ast.Node;
-import com.vladsch.flexmark.util.data.MutableDataHolder;
-import com.vladsch.flexmark.util.html.MutableAttributes;
-import org.jetbrains.annotations.NotNull;
-
-import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX;
-import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
-import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
-import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
-
-/**
- * Responsible for giving most block-level elements a unique identifier
- * attribute. The identifier is used to coordinate scrolling.
- */
-public class BlockExtension implements HtmlRendererExtension {
- /**
- * Responsible for creating the id attribute. This class is instantiated
- * each time the document is rendered, thereby resetting the count to zero.
- */
- public static class IdAttributeProvider implements AttributeProvider {
- private int mCount;
-
- private static AttributeProviderFactory createFactory() {
- return new IndependentAttributeProviderFactory() {
- @Override
- public @NotNull AttributeProvider apply(
- @NotNull final LinkResolverContext context ) {
- return new IdAttributeProvider();
- }
- };
- }
-
- @Override
- public void setAttributes( @NotNull Node node,
- @NotNull AttributablePart part,
- @NotNull MutableAttributes attributes ) {
- // Blockquotes are troublesome because they can interleave blank lines
- // without having an equivalent blank line in the source document. That
- // is, in Markdown the > symbol on a line by itself will generate a blank
- // line in the resulting document; however, a > symbol in the text editor
- // does not count as a blank line. Resolving this issue is tricky.
- //
- // The CODE_CONTENT represents <code> embedded inside <pre>; both elements
- // enter this method as FencedCodeBlock, but only the <pre> must be
- // uniquely identified (because they are the same line in Markdown).
- //
- if( node instanceof Block &&
- !(node instanceof BlockQuote) &&
- !(node instanceof ListBlock) &&
- (part != CODE_CONTENT) ) {
- attributes.addValue( "id", PARAGRAPH_ID_PREFIX + mCount++ );
- }
- }
- }
-
- private BlockExtension() {
- }
-
- @Override
- public void extend( final Builder builder,
- @NotNull final String rendererType ) {
- builder.attributeProviderFactory( IdAttributeProvider.createFactory() );
- }
-
- public static BlockExtension create() {
- return new BlockExtension();
- }
-
- @Override
- public void rendererOptions( @NotNull final MutableDataHolder options ) {
- }
-}
src/main/java/com/scrivenvar/processors/markdown/ImageLinkExtension.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.markdown;
-
-import com.scrivenvar.preferences.UserPreferences;
-import com.vladsch.flexmark.ast.Image;
-import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
-import com.vladsch.flexmark.html.LinkResolver;
-import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
-import com.vladsch.flexmark.html.renderer.LinkStatus;
-import com.vladsch.flexmark.html.renderer.ResolvedLink;
-import com.vladsch.flexmark.util.ast.Node;
-import com.vladsch.flexmark.util.data.MutableDataHolder;
-import org.jetbrains.annotations.NotNull;
-import org.renjin.repackaged.guava.base.Splitter;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.nio.file.Path;
-
-import static com.scrivenvar.StatusBarNotifier.alert;
-import static com.scrivenvar.util.ProtocolResolver.getProtocol;
-import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
-import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
-import static java.lang.String.format;
-
-/**
- * Responsible for ensuring that images can be rendered relative to a path.
- * This allows images to be located virtually anywhere.
- */
-public class ImageLinkExtension implements HtmlRendererExtension {
-
- /**
- * Creates an extension capable of using a relative path to embed images.
- *
- * @param path The {@link Path} to the file being edited; the parent path
- * is the starting location of the relative image directory.
- * @return The new {@link ImageLinkExtension}, never {@code null}.
- */
- public static ImageLinkExtension create( @NotNull final Path path ) {
- return new ImageLinkExtension( path );
- }
-
- private class Factory extends IndependentLinkResolverFactory {
- @Override
- public @NotNull LinkResolver apply(
- @NotNull final LinkResolverBasicContext context ) {
- return new ImageLinkResolver();
- }
- }
-
- private class ImageLinkResolver implements LinkResolver {
- private final UserPreferences mUserPref = getUserPreferences();
- private final File mImagesUserPrefix = mUserPref.getImagesDirectory();
- private final String mImageExtensions = mUserPref.getImagesOrder();
-
- public ImageLinkResolver() {
- }
-
- /**
- * You can also set/clear/modify attributes through
- * {@link ResolvedLink#getAttributes()} and
- * {@link ResolvedLink#getNonNullAttributes()}.
- */
- @NotNull
- @Override
- public ResolvedLink resolveLink(
- @NotNull final Node node,
- @NotNull final LinkResolverBasicContext context,
- @NotNull final ResolvedLink link ) {
- return node instanceof Image ? resolve( link ) : link;
- }
-
- private ResolvedLink resolve( final ResolvedLink link ) {
- var url = link.getUrl();
- final var protocol = getProtocol( url );
-
- try {
- // If the direct file name exists, then use it directly.
- if( (protocol.isFile() && Path.of( url ).toFile().exists()) ||
- protocol.isHttp() ) {
- return valid( link, url );
- }
- } catch( final Exception ignored ) {
- // Try to resolve the image, dynamically.
- }
-
- try {
- final Path imagePrefix = getImagePrefix().toPath();
-
- // Path to the file being edited.
- Path editPath = getEditPath();
-
- // If there is no parent path to the file, it means the file has not
- // been saved. Default to using the value from the user's preferences.
- // The user's preferences will be defaulted to a the application's
- // starting directory.
- if( editPath == null ) {
- editPath = imagePrefix;
- }
- else {
- editPath = Path.of( editPath.toString(), imagePrefix.toString() );
- }
-
- final Path imagePathPrefix = Path.of( editPath.toString(), url );
- final String suffixes = getImageExtensions();
- boolean missing = true;
-
- // Iterate over the user's preferred image file type extensions.
- for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) {
- final String imagePath = format( "%s.%s", imagePathPrefix, ext );
- final File file = new File( imagePath );
-
- if( file.exists() ) {
- url = file.toString();
- missing = false;
- break;
- }
- }
-
- if( missing ) {
- throw new FileNotFoundException( imagePathPrefix + ".*" );
- }
-
- if( protocol.isFile() ) {
- url = "file://" + url;
- }
-
- return valid( link, url );
- } catch( final Exception ex ) {
- alert( ex );
- }
-
- return link;
- }
-
- private ResolvedLink valid( final ResolvedLink link, final String url ) {
- return link.withStatus( LinkStatus.VALID ).withUrl( url );
- }
-
- private File getImagePrefix() {
- return mImagesUserPrefix;
- }
-
- private String getImageExtensions() {
- return mImageExtensions;
- }
-
- private Path getEditPath() {
- return mPath.getParent();
- }
- }
-
- private final Path mPath;
-
- private ImageLinkExtension( @NotNull final Path path ) {
- mPath = path;
- }
-
- @Override
- public void rendererOptions( @NotNull final MutableDataHolder options ) {
- }
-
- @Override
- public void extend( @NotNull final Builder builder,
- @NotNull final String rendererType ) {
- builder.linkResolverFactory( new Factory() );
- }
-
- private UserPreferences getUserPreferences() {
- return UserPreferences.getInstance();
- }
-}
src/main/java/com/scrivenvar/processors/markdown/LigatureExtension.java
-package com.scrivenvar.processors.markdown;
-
-import com.vladsch.flexmark.ast.Text;
-import com.vladsch.flexmark.html.HtmlWriter;
-import com.vladsch.flexmark.html.renderer.NodeRenderer;
-import com.vladsch.flexmark.html.renderer.NodeRendererContext;
-import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
-import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
-import com.vladsch.flexmark.util.ast.TextCollectingVisitor;
-import com.vladsch.flexmark.util.data.DataHolder;
-import com.vladsch.flexmark.util.data.MutableDataHolder;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Set;
-
-import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
-import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
-import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
-
-/**
- * Responsible for substituting multi-codepoint glyphs with single codepoint
- * glyphs. The text is adorned with ligatures prior to rendering as HTML.
- * This requires a font that supports ligatures.
- * <p>
- * TODO: I18N https://github.com/DaveJarvis/scrivenvar/issues/81
- * </p>
- */
-public class LigatureExtension implements HtmlRendererExtension {
- /**
- * Retain insertion order so that ligature substitution uses longer ligatures
- * ahead of shorter ligatures. The word "ruffian" should use the "ffi"
- * ligature, not the "ff" ligature.
- */
- private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
-
- static {
- LIGATURES.put( "ffi", "\uFB03" );
- LIGATURES.put( "ffl", "\uFB04" );
- LIGATURES.put( "ff", "\uFB00" );
- LIGATURES.put( "fi", "\uFB01" );
- LIGATURES.put( "fl", "\uFB02" );
- LIGATURES.put( "ft", "\uFB05" );
- LIGATURES.put( "AE", "\u00C6" );
- LIGATURES.put( "OE", "\u0152" );
-// "ae", "\u00E6",
-// "oe", "\u0153",
- }
-
- private static class LigatureRenderer implements NodeRenderer {
- private final TextCollectingVisitor mVisitor = new TextCollectingVisitor();
-
- @SuppressWarnings("unused")
- public LigatureRenderer( final DataHolder options ) {
- }
-
- @Override
- public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
- return Set.of( new NodeRenderingHandler<>(
- Text.class, LigatureRenderer.this::render ) );
- }
-
- /**
- * This will pick the fastest string replacement algorithm based on the
- * text length. The insertion order of the {@link #LIGATURES} is
- * important to give precedence to longer ligatures.
- *
- * @param textNode The text node containing text to replace with ligatures.
- * @param context Not used.
- * @param html Where to write the text adorned with ligatures.
- */
- private void render(
- @NotNull final Text textNode,
- @NotNull final NodeRendererContext context,
- @NotNull final HtmlWriter html ) {
- final var text = mVisitor.collectAndGetText( textNode );
- html.text( replace( text, LIGATURES ) );
- }
- }
-
- private static class Factory implements NodeRendererFactory {
- @NotNull
- @Override
- public NodeRenderer apply( @NotNull DataHolder options ) {
- return new LigatureRenderer( options );
- }
- }
-
- private LigatureExtension() {
- }
-
- @Override
- public void rendererOptions( @NotNull final MutableDataHolder options ) {
- }
-
- @Override
- public void extend( @NotNull final Builder builder,
- @NotNull final String rendererType ) {
- if( "HTML".equalsIgnoreCase( rendererType ) ) {
- builder.nodeRendererFactory( new Factory() );
- }
- }
-
- public static LigatureExtension create() {
- return new LigatureExtension();
- }
-}
src/main/java/com/scrivenvar/processors/markdown/MarkdownProcessor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.markdown;
-
-import com.scrivenvar.processors.AbstractProcessor;
-import com.scrivenvar.processors.Processor;
-import com.vladsch.flexmark.ext.definition.DefinitionExtension;
-import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
-import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
-import com.vladsch.flexmark.ext.tables.TablesExtension;
-import com.vladsch.flexmark.ext.typographic.TypographicExtension;
-import com.vladsch.flexmark.html.HtmlRenderer;
-import com.vladsch.flexmark.parser.Parser;
-import com.vladsch.flexmark.util.ast.IParse;
-import com.vladsch.flexmark.util.ast.Node;
-import com.vladsch.flexmark.util.misc.Extension;
-
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collection;
-
-import static com.scrivenvar.Constants.USER_DIRECTORY;
-
-/**
- * Responsible for parsing a Markdown document and rendering it as HTML.
- */
-public class MarkdownProcessor extends AbstractProcessor<String> {
-
- private final HtmlRenderer mRenderer;
- private final IParse mParser;
-
- public MarkdownProcessor(
- final Processor<String> successor ) {
- this( successor, Path.of( USER_DIRECTORY ) );
- }
-
- /**
- * Constructs a new Markdown processor that can create HTML documents.
- *
- * @param successor Usually the HTML Preview Processor.
- */
- public MarkdownProcessor(
- final Processor<String> successor, final Path path ) {
- super( successor );
-
- // Standard extensions
- final Collection<Extension> extensions = new ArrayList<>();
- extensions.add( DefinitionExtension.create() );
- extensions.add( StrikethroughSubscriptExtension.create() );
- extensions.add( SuperscriptExtension.create() );
- extensions.add( TablesExtension.create() );
- extensions.add( TypographicExtension.create() );
-
- // Allows referencing image files via relative paths and dynamic file types.
- extensions.add( ImageLinkExtension.create( path ) );
- extensions.add( BlockExtension.create() );
- extensions.add( TeXExtension.create() );
-
- // TODO: https://github.com/FAlthausen/Vollkorn-Typeface/issues/38
- // TODO: Uncomment when Vollkorn ligatures are fixed.
- // extensions.add( LigatureExtension.create() );
-
- mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
- mParser = Parser.builder()
- .extensions( extensions )
- .build();
- }
-
- /**
- * Converts the given Markdown string into HTML, without the doctype, html,
- * head, and body tags.
- *
- * @param markdown The string to convert from Markdown to HTML.
- * @return The HTML representation of the Markdown document.
- */
- @Override
- public String apply( final String markdown ) {
- return toHtml( markdown );
- }
-
- /**
- * Returns the AST in the form of a node for the given markdown document. This
- * can be used, for example, to determine if a hyperlink exists inside of a
- * paragraph.
- *
- * @param markdown The markdown to convert into an AST.
- * @return The markdown AST for the given text (usually a paragraph).
- */
- public Node toNode( final String markdown ) {
- return parse( markdown );
- }
-
- /**
- * Helper method to create an AST given some markdown.
- *
- * @param markdown The markdown to parse.
- * @return The root node of the markdown tree.
- */
- private Node parse( final String markdown ) {
- return getParser().parse( markdown );
- }
-
- /**
- * Converts a string of markdown into HTML.
- *
- * @param markdown The markdown text to convert to HTML, must not be null.
- * @return The markdown rendered as an HTML document.
- */
- private String toHtml( final String markdown ) {
- return getRenderer().render( parse( markdown ) );
- }
-
- /**
- * Creates the Markdown document processor.
- *
- * @return A Parser that can build an abstract syntax tree.
- */
- private IParse getParser() {
- return mParser;
- }
-
- private HtmlRenderer getRenderer() {
- return mRenderer;
- }
-}
src/main/java/com/scrivenvar/processors/markdown/TeXExtension.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.markdown;
-
-import com.scrivenvar.processors.markdown.tex.TeXInlineDelimiterProcessor;
-import com.scrivenvar.processors.markdown.tex.TeXNodeRenderer;
-import com.vladsch.flexmark.html.HtmlRenderer;
-import com.vladsch.flexmark.parser.Parser;
-import com.vladsch.flexmark.util.data.MutableDataHolder;
-import org.jetbrains.annotations.NotNull;
-
-import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension;
-import static com.vladsch.flexmark.parser.Parser.ParserExtension;
-
-/**
- * Responsible for wrapping delimited TeX code in Markdown into an XML element
- * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes
- * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer
- * is responsible for converting the TeX code for display. This avoids inserting
- * SVG code into the Markdown document, which the parser would then have to
- * iterate---a <em>very</em> wasteful operation that impacts front-end
- * performance.
- */
-public class TeXExtension implements ParserExtension, HtmlRendererExtension {
- /**
- * Creates an extension capable of handling delimited TeX code in Markdown.
- *
- * @return The new {@link TeXExtension}, never {@code null}.
- */
- public static TeXExtension create() {
- return new TeXExtension();
- }
-
- /**
- * Force using the {@link #create()} method for consistency.
- */
- private TeXExtension() {
- }
-
- /**
- * Adds the TeX extension for HTML document export types.
- *
- * @param builder The document builder.
- * @param rendererType Indicates the document type to be built.
- */
- @Override
- public void extend( @NotNull final HtmlRenderer.Builder builder,
- @NotNull final String rendererType ) {
- if( "HTML".equalsIgnoreCase( rendererType ) ) {
- builder.nodeRendererFactory( new TeXNodeRenderer.Factory() );
- }
- }
-
- @Override
- public void extend( final Parser.Builder builder ) {
- builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() );
- }
-
- @Override
- public void rendererOptions( @NotNull final MutableDataHolder options ) {
- }
-
- @Override
- public void parserOptions( final MutableDataHolder options ) {
- }
-}
src/main/java/com/scrivenvar/processors/markdown/tex/TeXInlineDelimiterProcessor.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.markdown.tex;
-
-import com.vladsch.flexmark.parser.InlineParser;
-import com.vladsch.flexmark.parser.core.delimiter.Delimiter;
-import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor;
-import com.vladsch.flexmark.parser.delimiter.DelimiterRun;
-import com.vladsch.flexmark.util.ast.Node;
-
-public class TeXInlineDelimiterProcessor implements DelimiterProcessor {
-
- @Override
- public void process( final Delimiter opener, final Delimiter closer,
- final int delimitersUsed ) {
- final var node = new TeXNode();
- opener.moveNodesBetweenDelimitersTo(node, closer);
- }
-
- @Override
- public char getOpeningCharacter() {
- return '$';
- }
-
- @Override
- public char getClosingCharacter() {
- return '$';
- }
-
- @Override
- public int getMinLength() {
- return 1;
- }
-
- /**
- * Allow for $ or $$.
- *
- * @param opener One or more opening delimiter characters.
- * @param closer One or more closing delimiter characters.
- * @return The number of delimiters to use to determine whether a valid
- * opening delimiter expression is found.
- */
- @Override
- public int getDelimiterUse(
- final DelimiterRun opener, final DelimiterRun closer ) {
- return 1;
- }
-
- @Override
- public boolean canBeOpener( final String before,
- final String after,
- final boolean leftFlanking,
- final boolean rightFlanking,
- final boolean beforeIsPunctuation,
- final boolean afterIsPunctuation,
- final boolean beforeIsWhitespace,
- final boolean afterIsWhiteSpace ) {
- return leftFlanking;
- }
-
- @Override
- public boolean canBeCloser( final String before,
- final String after,
- final boolean leftFlanking,
- final boolean rightFlanking,
- final boolean beforeIsPunctuation,
- final boolean afterIsPunctuation,
- final boolean beforeIsWhitespace,
- final boolean afterIsWhiteSpace ) {
- return rightFlanking;
- }
-
- @Override
- public Node unmatchedDelimiterNode(
- final InlineParser inlineParser, final DelimiterRun delimiter ) {
- return null;
- }
-
- @Override
- public boolean skipNonOpenerCloser() {
- return false;
- }
-}
src/main/java/com/scrivenvar/processors/markdown/tex/TeXNode.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.markdown.tex;
-
-import com.vladsch.flexmark.ast.DelimitedNodeImpl;
-
-public class TeXNode extends DelimitedNodeImpl {
- /**
- * TeX expression wrapped in a {@code <tex>} element.
- */
- public static final String HTML_TEX = "tex";
-
- public TeXNode() {
- }
-}
src/main/java/com/scrivenvar/processors/markdown/tex/TeXNodeRenderer.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.markdown.tex;
-
-import com.vladsch.flexmark.html.HtmlWriter;
-import com.vladsch.flexmark.html.renderer.NodeRenderer;
-import com.vladsch.flexmark.html.renderer.NodeRendererContext;
-import com.vladsch.flexmark.html.renderer.NodeRendererFactory;
-import com.vladsch.flexmark.html.renderer.NodeRenderingHandler;
-import com.vladsch.flexmark.util.data.DataHolder;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.Set;
-
-import static com.scrivenvar.processors.markdown.tex.TeXNode.HTML_TEX;
-
-public class TeXNodeRenderer implements NodeRenderer {
-
- public static class Factory implements NodeRendererFactory {
- @NotNull
- @Override
- public NodeRenderer apply( @NotNull DataHolder options ) {
- return new TeXNodeRenderer();
- }
- }
-
- @Override
- public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() {
- return Set.of( new NodeRenderingHandler<>( TeXNode.class, this::render ) );
- }
-
- private void render( final TeXNode node,
- final NodeRendererContext context,
- final HtmlWriter html ) {
- html.tag( HTML_TEX );
- html.raw( node.getText() );
- html.closeTag( HTML_TEX );
- }
-}
src/main/java/com/scrivenvar/processors/text/AbstractTextReplacer.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.text;
-
-import java.util.Map;
-
-/**
- * Responsible for common behaviour across all text replacer implementations.
- */
-public abstract class AbstractTextReplacer implements TextReplacer {
-
- /**
- * Default (empty) constructor.
- */
- protected AbstractTextReplacer() {
- }
-
- protected String[] keys( final Map<String, String> map ) {
- return map.keySet().toArray( new String[ 0 ] );
- }
-
- protected String[] values( final Map<String, String> map ) {
- return map.values().toArray( new String[ 0 ] );
- }
-}
src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.text;
-
-import java.util.Map;
-import org.ahocorasick.trie.Emit;
-import org.ahocorasick.trie.Trie.TrieBuilder;
-import static org.ahocorasick.trie.Trie.builder;
-
-/**
- * Replaces text using an Aho-Corasick algorithm.
- */
-public class AhoCorasickReplacer extends AbstractTextReplacer {
-
- /**
- * Default (empty) constructor.
- */
- protected AhoCorasickReplacer() {
- }
-
- @Override
- public String replace( final String text, final Map<String, String> map ) {
- // Create a buffer sufficiently large that re-allocations are minimized.
- final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
-
- // The TrieBuilder should only match whole words and ignore overlaps (there
- // shouldn't be any).
- final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps();
-
- for( final String key : keys( map ) ) {
- builder.addKeyword( key );
- }
-
- int index = 0;
-
- // Replace all instances with dereferenced variables.
- for( final Emit emit : builder.build().parseText( text ) ) {
- sb.append( text, index, emit.getStart() );
- sb.append( map.get( emit.getKeyword() ) );
- index = emit.getEnd() + 1;
- }
-
- // Add the remainder of the string (contains no more matches).
- sb.append( text.substring( index ) );
-
- return sb.toString();
- }
-}
src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.text;
-
-import java.util.Map;
-
-import static org.apache.commons.lang3.StringUtils.replaceEach;
-
-/**
- * Replaces text using Apache's StringUtils.replaceEach method.
- */
-public class StringUtilsReplacer extends AbstractTextReplacer {
-
- /**
- * Default (empty) constructor.
- */
- protected StringUtilsReplacer() {
- }
-
- @Override
- public String replace( final String text, final Map<String, String> map ) {
- return replaceEach( text, keys( map ), values( map ) );
- }
-}
src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.text;
-
-import java.util.Map;
-
-/**
- * Used to generate a class capable of efficiently replacing variable
- * definitions with their values.
- */
-public final class TextReplacementFactory {
-
- private static final TextReplacer APACHE = new StringUtilsReplacer();
- private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
-
- /**
- * Returns a text search/replacement instance that is reasonably optimal for
- * the given length of text.
- *
- * @param length The length of text that requires some search and replacing.
- * @return A class that can search and replace text with utmost expediency.
- */
- public static TextReplacer getTextReplacer( final int length ) {
- // After about 1,500 characters, the StringUtils implementation is less
- // performant than the Aho-Corsick implementation.
- //
- // See http://stackoverflow.com/a/40836618/59087
- return length < 1500 ? APACHE : AHO_CORASICK;
- }
-
- /**
- * Convenience method to instantiate a suitable text replacer algorithm and
- * perform a replacement using the given map. At this point, the values should
- * be already dereferenced and ready to be substituted verbatim; any
- * recursively defined values must have been interpolated previously.
- *
- * @param text The text containing zero or more variables to replace.
- * @param map The map of variables to their dereferenced values.
- * @return The text with all variables replaced.
- */
- public static String replace(
- final String text, final Map<String, String> map ) {
- return getTextReplacer( text.length() ).replace( text, map );
- }
-}
src/main/java/com/scrivenvar/processors/text/TextReplacer.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.processors.text;
-
-import java.util.Map;
-
-/**
- * Defines the ability to replace text given a set of keys and values.
- */
-public interface TextReplacer {
-
- /**
- * Searches through the given text for any of the keys given in the map and
- * replaces the keys that appear in the text with the key's corresponding
- * value.
- *
- * @param text The text that contains zero or more keys.
- * @param map The set of keys mapped to replacement values.
- * @return The given text with all keys replaced with corresponding values.
- */
- String replace( String text, Map<String, String> map );
-}
src/main/java/com/scrivenvar/service/Options.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service;
-
-import com.dlsc.preferencesfx.PreferencesFx;
-
-import java.util.prefs.BackingStoreException;
-import java.util.prefs.Preferences;
-
-/**
- * Responsible for persisting options that are safe to load before the UI
- * is shown. This can include items like window dimensions, last file
- * opened, split pane locations, and more. This cannot be used to persist
- * options that are user-controlled (i.e., all options available through
- * {@link PreferencesFx}).
- */
-public interface Options extends Service {
-
- /**
- * Returns the {@link Preferences} that persist settings that cannot
- * be configured via the user interface.
- *
- * @return A valid {@link Preferences} instance, never {@code null}.
- */
- Preferences getState();
-
- /**
- * Stores the key and value into the user preferences to be loaded the next
- * time the application is launched.
- *
- * @param key Name of the key to persist along with its value.
- * @param value Value to associate with the key.
- * @throws BackingStoreException Could not persist the change.
- */
- void put( String key, String value ) throws BackingStoreException;
-
- /**
- * Retrieves the value for a key in the user preferences.
- *
- * @param key Retrieve the value of this key.
- * @param defaultValue The value to return in the event that the given key has
- * no associated value.
- * @return The value associated with the key.
- */
- String get( String key, String defaultValue );
-
- /**
- * Retrieves the value for a key in the user preferences. This will return
- * the empty string if the value cannot be found.
- *
- * @param key The key to find in the preferences.
- * @return A non-null, possibly empty value for the key.
- */
- String get( String key );
-}
src/main/java/com/scrivenvar/service/Service.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service;
-
-/**
- * All services inherit from this one.
- */
-public interface Service {
-}
src/main/java/com/scrivenvar/service/Settings.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service;
-
-import java.util.Iterator;
-import java.util.List;
-
-/**
- * Defines how settings and options can be retrieved.
- */
-public interface Settings extends Service {
-
- /**
- * Returns a setting property or its default value.
- *
- * @param property The property key name to obtain its value.
- * @param defaultValue The default value to return iff the property cannot be
- * found.
- * @return The property value for the given property key.
- */
- String getSetting( String property, String defaultValue );
-
- /**
- * Returns a setting property or its default value.
- *
- * @param property The property key name to obtain its value.
- * @param defaultValue The default value to return iff the property cannot be
- * found.
- * @return The property value for the given property key.
- */
- int getSetting( String property, int defaultValue );
-
- /**
- * Returns a list of property names that begin with the given prefix. The
- * prefix is included in any matching results. This will return keys that
- * either match the prefix or start with the prefix followed by a dot ('.').
- * For example, a prefix value of <code>the.property.name</code> will likely
- * return the expected results, but <code>the.property.name.</code> (note the
- * extraneous period) will probably not.
- *
- * @param prefix The prefix to compare against each property name.
- * @return The list of property names that have the given prefix.
- */
- Iterator<String> getKeys( final String prefix );
-
- /**
- * Convert the generic list of property objects into strings.
- *
- * @param property The property value to coerce.
- * @param defaults The defaults values to use should the property be unset.
- * @return The list of properties coerced from objects to strings.
- */
- List<String> getStringSettingList( String property, List<String> defaults );
-
- /**
- * Converts the generic list of property objects into strings.
- *
- * @param property The property value to coerce.
- * @return The list of properties coerced from objects to strings.
- */
- List<String> getStringSettingList( String property );
-}
src/main/java/com/scrivenvar/service/Snitch.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service;
-
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Observer;
-
-/**
- * Listens for changes to file system files and directories.
- */
-public interface Snitch extends Service, Runnable {
-
- /**
- * Adds an observer to the set of observers for this object, provided that it
- * is not the same as some observer already in the set. The order in which
- * notifications will be delivered to multiple observers is not specified.
- *
- * @param o The object to receive changed events for when monitored files
- * are changed.
- */
- void addObserver( Observer o );
-
- /**
- * Listens for changes to the path. If the path specifies a file, then only
- * notifications pertaining to that file are sent. Otherwise, change events
- * for the directory that contains the file are sent. This method must allow
- * for multiple calls to the same file without incurring additional listeners
- * or events.
- *
- * @param file Send notifications when this file changes, can be null.
- * @throws IOException Couldn't create a watcher for the given file.
- */
- void listen( Path file ) throws IOException;
-
- /**
- * Removes the given file from the notifications list.
- *
- * @param file The file to stop monitoring for any changes, can be null.
- */
- void ignore( Path file );
-
- /**
- * Stop listening for events.
- */
- void stop();
-}
src/main/java/com/scrivenvar/service/events/Notification.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service.events;
-
-/**
- * Represents a message that contains a title and content.
- */
-public interface Notification {
-
- /**
- * Alert title.
- *
- * @return A non-null string to use as alert message title.
- */
- String getTitle();
-
- /**
- * Alert message content.
- *
- * @return A non-null string that contains information for the user.
- */
- String getContent();
-}
src/main/java/com/scrivenvar/service/events/Notifier.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service.events;
-
-import javafx.scene.control.Alert;
-import javafx.scene.control.ButtonType;
-import javafx.stage.Window;
-
-/**
- * Provides the application with a uniform way to notify the user of events.
- */
-public interface Notifier {
-
- ButtonType YES = ButtonType.YES;
- ButtonType NO = ButtonType.NO;
- ButtonType CANCEL = ButtonType.CANCEL;
-
- /**
- * Constructs a default alert message text for a modal alert dialog.
- *
- * @param title The dialog box message title.
- * @param message The dialog box message content (needs formatting).
- * @param args The arguments to the message content that must be formatted.
- * @return The message suitable for building a modal alert dialog.
- */
- Notification createNotification(
- String title,
- String message,
- Object... args );
-
- /**
- * Creates an alert of alert type error with a message showing the cause of
- * the error.
- *
- * @param parent Dialog box owner (for modal purposes).
- * @param message The error message, title, and possibly more details.
- * @return A modal alert dialog box ready to display using showAndWait.
- */
- Alert createError( Window parent, Notification message );
-
- /**
- * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
- *
- * @param parent Dialog box owner (for modal purposes).
- * @param message The message, title, and possibly more details.
- * @return A modal alert dialog box ready to display using showAndWait.
- */
- Alert createConfirmation( Window parent, Notification message );
-}
src/main/java/com/scrivenvar/service/events/impl/ButtonOrderPane.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service.events.impl;
-
-import javafx.scene.Node;
-import javafx.scene.control.ButtonBar;
-import javafx.scene.control.DialogPane;
-
-import static com.scrivenvar.Constants.SETTINGS;
-import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
-
-/**
- * Ensures a consistent button order for alert dialogs across platforms (because
- * the default button order on Linux defies all logic).
- */
-public class ButtonOrderPane extends DialogPane {
-
- @Override
- protected Node createButtonBar() {
- final var node = (ButtonBar) super.createButtonBar();
- node.setButtonOrder( getButtonOrder() );
- return node;
- }
-
- private String getButtonOrder() {
- return getSetting( "dialog.alert.button.order.windows",
- BUTTON_ORDER_WINDOWS );
- }
-
- @SuppressWarnings("SameParameterValue")
- private String getSetting( final String key, final String defaultValue ) {
- return SETTINGS.getSetting( key, defaultValue );
- }
-}
src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service.events.impl;
-
-import com.scrivenvar.service.events.Notification;
-
-import java.text.MessageFormat;
-
-/**
- * Responsible for alerting the user to prominent information.
- */
-public class DefaultNotification implements Notification {
-
- private final String title;
- private final String content;
-
- /**
- * Constructs default message text for a notification.
- *
- * @param title The message title.
- * @param message The message content (needs formatting).
- * @param args The arguments to the message content that must be formatted.
- */
- public DefaultNotification(
- final String title,
- final String message,
- final Object... args ) {
- this.title = title;
- this.content = MessageFormat.format( message, args );
- }
-
- @Override
- public String getTitle() {
- return this.title;
- }
-
- @Override
- public String getContent() {
- return this.content;
- }
-}
src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service.events.impl;
-
-import com.scrivenvar.service.events.Notification;
-import com.scrivenvar.service.events.Notifier;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Alert.AlertType;
-import javafx.stage.Window;
-
-import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
-import static javafx.scene.control.Alert.AlertType.ERROR;
-
-/**
- * Provides the ability to notify the user of events that need attention,
- * such as prompting the user to confirm closing when there are unsaved changes.
- */
-public final class DefaultNotifier implements Notifier {
-
- /**
- * Contains all the information that the user needs to know about a problem.
- *
- * @param title The context for the message.
- * @param message The message content (formatted with the given args).
- * @param args Parameters for the message content.
- * @return A notification instance, never null.
- */
- @Override
- public Notification createNotification(
- final String title,
- final String message,
- final Object... args ) {
- return new DefaultNotification( title, message, args );
- }
-
- private Alert createAlertDialog(
- final Window parent,
- final AlertType alertType,
- final Notification message ) {
-
- final Alert alert = new Alert( alertType );
-
- alert.setDialogPane( new ButtonOrderPane() );
- alert.setTitle( message.getTitle() );
- alert.setHeaderText( null );
- alert.setContentText( message.getContent() );
- alert.initOwner( parent );
-
- return alert;
- }
-
- @Override
- public Alert createConfirmation( final Window parent,
- final Notification message ) {
- final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
-
- alert.getButtonTypes().setAll( YES, NO, CANCEL );
-
- return alert;
- }
-
- @Override
- public Alert createError( final Window parent, final Notification message ) {
- return createAlertDialog( parent, ERROR, message );
- }
-}
src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service.impl;
-
-import com.scrivenvar.service.Options;
-
-import java.util.prefs.BackingStoreException;
-import java.util.prefs.Preferences;
-
-import static com.scrivenvar.Constants.PREFS_ROOT;
-import static com.scrivenvar.Constants.PREFS_STATE;
-import static java.util.prefs.Preferences.userRoot;
-
-/**
- * Persistent options user can change at runtime.
- */
-public class DefaultOptions implements Options {
- public DefaultOptions() {
- }
-
- /**
- * This will throw IllegalArgumentException if the value exceeds the maximum
- * preferences value length.
- *
- * @param key The name of the key to associate with the value.
- * @param value The value to persist.
- * @throws BackingStoreException New value not persisted.
- */
- @Override
- public void put( final String key, final String value )
- throws BackingStoreException {
- getState().put( key, value );
- getState().flush();
- }
-
- @Override
- public String get( final String key, final String value ) {
- return getState().get( key, value );
- }
-
- @Override
- public String get( final String key ) {
- return get( key, "" );
- }
-
- private Preferences getRootPreferences() {
- return userRoot().node( PREFS_ROOT );
- }
-
- @Override
- public Preferences getState() {
- return getRootPreferences().node( PREFS_STATE );
- }
-}
src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service.impl;
-
-import com.scrivenvar.service.Settings;
-import org.apache.commons.configuration2.PropertiesConfiguration;
-import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
-import org.apache.commons.configuration2.convert.ListDelimiterHandler;
-import org.apache.commons.configuration2.ex.ConfigurationException;
-
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.net.URL;
-import java.nio.charset.Charset;
-import java.util.Iterator;
-import java.util.List;
-
-import static com.scrivenvar.Constants.SETTINGS_NAME;
-
-/**
- * Responsible for loading settings that help avoid hard-coded assumptions.
- */
-public class DefaultSettings implements Settings {
-
- private static final char VALUE_SEPARATOR = ',';
-
- private PropertiesConfiguration properties;
-
- public DefaultSettings() throws ConfigurationException {
- setProperties( createProperties() );
- }
-
- /**
- * Returns the value of a string property.
- *
- * @param property The property key.
- * @param defaultValue The value to return if no property key has been set.
- * @return The property key value, or defaultValue when no key found.
- */
- @Override
- public String getSetting( final String property, final String defaultValue ) {
- return getSettings().getString( property, defaultValue );
- }
-
- /**
- * Returns the value of a string property.
- *
- * @param property The property key.
- * @param defaultValue The value to return if no property key has been set.
- * @return The property key value, or defaultValue when no key found.
- */
- @Override
- public int getSetting( final String property, final int defaultValue ) {
- return getSettings().getInt( property, defaultValue );
- }
-
- /**
- * Convert the generic list of property objects into strings.
- *
- * @param property The property value to coerce.
- * @param defaults The defaults values to use should the property be unset.
- * @return The list of properties coerced from objects to strings.
- */
- @Override
- public List<String> getStringSettingList(
- final String property, final List<String> defaults ) {
- return getSettings().getList( String.class, property, defaults );
- }
-
- /**
- * Convert a list of property objects into strings, with no default value.
- *
- * @param property The property value to coerce.
- * @return The list of properties coerced from objects to strings.
- */
- @Override
- public List<String> getStringSettingList( final String property ) {
- return getStringSettingList( property, null );
- }
-
- /**
- * Returns a list of property names that begin with the given prefix.
- *
- * @param prefix The prefix to compare against each property name.
- * @return The list of property names that have the given prefix.
- */
- @Override
- public Iterator<String> getKeys( final String prefix ) {
- return getSettings().getKeys( prefix );
- }
-
- private PropertiesConfiguration createProperties()
- throws ConfigurationException {
-
- final URL url = getPropertySource();
- final PropertiesConfiguration configuration = new PropertiesConfiguration();
-
- if( url != null ) {
- try( final Reader r = new InputStreamReader( url.openStream(),
- getDefaultEncoding() ) ) {
- configuration.setListDelimiterHandler( createListDelimiterHandler() );
- configuration.read( r );
-
- } catch( final IOException ex ) {
- throw new RuntimeException( new ConfigurationException( ex ) );
- }
- }
-
- return configuration;
- }
-
- protected Charset getDefaultEncoding() {
- return Charset.defaultCharset();
- }
-
- protected ListDelimiterHandler createListDelimiterHandler() {
- return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
- }
-
- private URL getPropertySource() {
- return DefaultSettings.class.getResource( getSettingsFilename() );
- }
-
- private String getSettingsFilename() {
- return SETTINGS_NAME;
- }
-
- private void setProperties( final PropertiesConfiguration configuration ) {
- this.properties = configuration;
- }
-
- private PropertiesConfiguration getSettings() {
- return this.properties;
- }
-}
src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.service.impl;
-
-import com.scrivenvar.service.Snitch;
-
-import java.io.IOException;
-import java.nio.file.*;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Observable;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT;
-import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
-
-/**
- * Listens for file changes. Other classes can register paths to be monitored
- * and listen for changes to those paths.
- */
-public class DefaultSnitch extends Observable implements Snitch {
-
- /**
- * Service for listening to directories for modifications.
- */
- private WatchService watchService;
-
- /**
- * Directories being monitored for changes.
- */
- private Map<WatchKey, Path> keys;
-
- /**
- * Files that will kick off notification events if modified.
- */
- private Set<Path> eavesdropped;
-
- /**
- * Set to true when running; set to false to stop listening.
- */
- private volatile boolean listening;
-
- public DefaultSnitch() {
- }
-
- @Override
- public void stop() {
- setListening( false );
- }
-
- /**
- * Adds a listener to the list of files to watch for changes. If the file is
- * already in the monitored list, this will return immediately.
- *
- * @param file Path to a file to watch for changes.
- * @throws IOException The file could not be monitored.
- */
- @Override
- public void listen( final Path file ) throws IOException {
- if( file != null && getEavesdropped().add( file ) ) {
- final Path dir = toDirectory( file );
- final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
-
- getWatchMap().put( key, dir );
- }
- }
-
- /**
- * Returns the given path to a file (or directory) as a directory. If the
- * given path is already a directory, it is returned. Otherwise, this returns
- * the directory that contains the file. This will fail if the file is stored
- * in the root folder.
- *
- * @param path The file to return as a directory, which should always be the
- * case.
- * @return The given path as a directory, if a file, otherwise the path
- * itself.
- */
- private Path toDirectory( final Path path ) {
- return Files.isDirectory( path )
- ? path
- : path.toFile().getParentFile().toPath();
- }
-
- /**
- * Stop listening to the given file for change events. This fails silently.
- *
- * @param file The file to no longer monitor for changes.
- */
- @Override
- public void ignore( final Path file ) {
- if( file != null ) {
- final Path directory = toDirectory( file );
-
- // Remove all occurrences (there should be only one).
- getWatchMap().values().removeAll( Collections.singleton( directory ) );
-
- // Remove all occurrences (there can be only one).
- getEavesdropped().remove( file );
- }
- }
-
- /**
- * Loops until stop is called, or the application is terminated.
- */
- @Override
- @SuppressWarnings("BusyWait")
- public void run() {
- setListening( true );
-
- while( isListening() ) {
- try {
- final WatchKey key = getWatchService().take();
- final Path path = get( key );
-
- // Prevent receiving two separate ENTRY_MODIFY events: file modified
- // and timestamp updated. Instead, receive one ENTRY_MODIFY event
- // with two counts.
- Thread.sleep( APP_WATCHDOG_TIMEOUT );
-
- for( final WatchEvent<?> event : key.pollEvents() ) {
- final Path changed = path.resolve( (Path) event.context() );
-
- if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
- setChanged();
- notifyObservers( changed );
- }
- }
-
- if( !key.reset() ) {
- ignore( path );
- }
- } catch( final IOException | InterruptedException ex ) {
- // Stop eavesdropping.
- setListening( false );
- }
- }
- }
-
- /**
- * Returns true if the list of files being listened to for changes contains
- * the given file.
- *
- * @param file Path to a system file.
- * @return true The given file is being monitored for changes.
- */
- private boolean isListening( final Path file ) {
- return getEavesdropped().contains( file );
- }
-
- /**
- * Returns a path for a given watch key.
- *
- * @param key The key to lookup its corresponding path.
- * @return The path for the given key.
- */
- private Path get( final WatchKey key ) {
- return getWatchMap().get( key );
- }
-
- private synchronized Map<WatchKey, Path> getWatchMap() {
- if( this.keys == null ) {
- this.keys = createWatchKeys();
- }
-
- return this.keys;
- }
-
- protected Map<WatchKey, Path> createWatchKeys() {
- return new ConcurrentHashMap<>();
- }
-
- /**
- * Returns a list of files that, when changed, will kick off a notification.
- *
- * @return A non-null, possibly empty, list of files.
- */
- private synchronized Set<Path> getEavesdropped() {
- if( this.eavesdropped == null ) {
- this.eavesdropped = createEavesdropped();
- }
-
- return this.eavesdropped;
- }
-
- protected Set<Path> createEavesdropped() {
- return ConcurrentHashMap.newKeySet();
- }
-
- /**
- * The existing watch service, or a new instance if null.
- *
- * @return A valid WatchService instance, never null.
- * @throws IOException Could not create a new watch service.
- */
- private synchronized WatchService getWatchService() throws IOException {
- if( this.watchService == null ) {
- this.watchService = createWatchService();
- }
-
- return this.watchService;
- }
-
- protected WatchService createWatchService() throws IOException {
- final FileSystem fileSystem = FileSystems.getDefault();
- return fileSystem.newWatchService();
- }
-
- /**
- * Answers whether the loop should continue executing.
- *
- * @return true The internal listening loop should continue listening for file
- * modification events.
- */
- protected boolean isListening() {
- return this.listening;
- }
-
- /**
- * Requests the snitch to stop eavesdropping on file changes.
- *
- * @param listening Use true to indicate the service should stop running.
- */
- private void setListening( final boolean listening ) {
- this.listening = listening;
- }
-}
src/main/java/com/scrivenvar/sigils/RSigilOperator.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.sigils;
-
-import static com.scrivenvar.sigils.YamlSigilOperator.KEY_SEPARATOR_DEF;
-
-/**
- * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils.
- */
-public class RSigilOperator extends SigilOperator {
- public static final char KEY_SEPARATOR_R = '$';
-
- public static final String PREFIX = "`r#";
- public static final char SUFFIX = '`';
-
- private final String mDelimiterBegan =
- getUserPreferences().getRDelimiterBegan();
- private final String mDelimiterEnded =
- getUserPreferences().getRDelimiterEnded();
-
- /**
- * Returns the given string R-escaping backticks prepended and appended. This
- * is not null safe. Do not pass null into this method.
- *
- * @param key The string to adorn with R token delimiters.
- * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`".
- */
- @Override
- public String apply( final String key ) {
- assert key != null;
-
- return PREFIX
- + mDelimiterBegan
- + entoken( key )
- + mDelimiterEnded
- + SUFFIX;
- }
-
- /**
- * Transforms a definition key (bracketed by token delimiters) into the
- * expected format for an R variable key name.
- *
- * @param key The variable name to transform, can be empty but not null.
- * @return The transformed variable name.
- */
- public static String entoken( final String key ) {
- return "v$" +
- YamlSigilOperator.detoken( key )
- .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R );
- }
-}
src/main/java/com/scrivenvar/sigils/SigilOperator.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.sigils;
-
-import com.scrivenvar.preferences.UserPreferences;
-
-import java.util.function.UnaryOperator;
-
-/**
- * Responsible for updating definition keys to use a machine-readable format
- * corresponding to the type of file being edited. This changes a definition
- * key name based on some criteria determined by the factory that creates
- * implementations of this interface.
- */
-public abstract class SigilOperator implements UnaryOperator<String> {
- protected static UserPreferences getUserPreferences() {
- return UserPreferences.getInstance();
- }
-}
src/main/java/com/scrivenvar/sigils/YamlSigilOperator.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.sigils;
-
-import java.util.regex.Pattern;
-
-import static java.lang.String.format;
-import static java.util.regex.Pattern.compile;
-import static java.util.regex.Pattern.quote;
-
-/**
- * Brackets definition keys with token delimiters.
- */
-public class YamlSigilOperator extends SigilOperator {
- public static final char KEY_SEPARATOR_DEF = '.';
-
- private static final String mDelimiterBegan =
- getUserPreferences().getDefDelimiterBegan();
- private static final String mDelimiterEnded =
- getUserPreferences().getDefDelimiterEnded();
-
- /**
- * Non-greedy match of key names delimited by definition tokens.
- */
- private static final String REGEX =
- format( "(%s.*?%s)", quote( mDelimiterBegan ), quote( mDelimiterEnded ) );
-
- /**
- * Compiled regular expression for matching delimited references.
- */
- public static final Pattern REGEX_PATTERN = compile( REGEX );
-
- /**
- * Returns the given {@link String} verbatim because variables in YAML
- * documents and plain Markdown documents already have the appropriate
- * tokenizable syntax wrapped around the text.
- *
- * @param key Returned verbatim.
- */
- @Override
- public String apply( final String key ) {
- return key;
- }
-
- /**
- * Adds delimiters to the given key.
- *
- * @param key The key to adorn with start and stop definition tokens.
- * @return The given key bracketed by definition token symbols.
- */
- public static String entoken( final String key ) {
- assert key != null;
- return mDelimiterBegan + key + mDelimiterEnded;
- }
-
- /**
- * Removes start and stop definition key delimiters from the given key. This
- * method does not check for delimiters, only that there are sufficient
- * characters to remove from either end of the given key.
- *
- * @param key The key adorned with start and stop definition tokens.
- * @return The given key with the delimiters removed.
- */
- public static String detoken( final String key ) {
- final int beganLen = mDelimiterBegan.length();
- final int endedLen = mDelimiterEnded.length();
-
- return key.length() > beganLen + endedLen
- ? key.substring( beganLen, key.length() - endedLen )
- : key;
- }
-}
src/main/java/com/scrivenvar/spelling/api/SpellCheckListener.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.spelling.api;
-
-import java.util.function.BiConsumer;
-
-/**
- * Represents an operation that accepts two input arguments and returns no
- * result. Unlike most other functional interfaces, this class is expected to
- * operate via side-effects.
- * <p>
- * This is used instead of a {@link BiConsumer} to avoid autoboxing.
- * </p>
- */
-@FunctionalInterface
-public interface SpellCheckListener {
-
- /**
- * Performs an operation on the given arguments.
- *
- * @param text The text associated with a beginning and ending offset.
- * @param beganOffset A starting offset, used as an index into a string.
- * @param endedOffset An ending offset, which should equal text.length() +
- * beganOffset.
- */
- void accept( String text, int beganOffset, int endedOffset );
-}
src/main/java/com/scrivenvar/spelling/api/SpellChecker.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.spelling.api;
-
-import java.util.List;
-
-/**
- * Defines the responsibilities for a spell checking API. The intention is
- * to allow different spell checking implementations to be used by the
- * application, such as SymSpell and LinSpell.
- */
-public interface SpellChecker {
-
- /**
- * Answers whether the given lexeme, in whole, is found in the lexicon. The
- * lexicon lookup is performed case-insensitively. This method should be
- * used instead of {@link #suggestions(String, int)} for performance reasons.
- *
- * @param lexeme The word to check for correctness.
- * @return {@code true} if the lexeme is in the lexicon.
- */
- boolean inLexicon( String lexeme );
-
- /**
- * Gets a list of spelling corrections for the given lexeme.
- *
- * @param lexeme A word to check for correctness that's not in the lexicon.
- * @param count The maximum number of alternatives to return.
- * @return A list of words in the lexicon that are similar to the given
- * lexeme.
- */
- List<String> suggestions( String lexeme, int count );
-
- /**
- * Iterates over the given text, emitting starting and ending offsets into
- * the text for every word that is missing from the lexicon.
- *
- * @param text The text to check for words missing from the lexicon.
- * @param consumer Every missing word emits a message with the starting
- * and ending offset into the text where said word is found.
- */
- void proofread( String text, SpellCheckListener consumer );
-}
src/main/java/com/scrivenvar/spelling/impl/PermissiveSpeller.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.spelling.impl;
-
-import com.scrivenvar.spelling.api.SpellCheckListener;
-import com.scrivenvar.spelling.api.SpellChecker;
-
-import java.util.List;
-
-/**
- * Responsible for spell checking in the event that a real spell checking
- * implementation cannot be created (for any reason). Does not perform any
- * spell checking and indicates that any given lexeme is in the lexicon.
- */
-public class PermissiveSpeller implements SpellChecker {
- /**
- * Returns {@code true}, ignoring the given word.
- *
- * @param ignored Unused.
- * @return {@code true}
- */
- @Override
- public boolean inLexicon( final String ignored ) {
- return true;
- }
-
- /**
- * Returns an array with the given lexeme.
- *
- * @param lexeme The word to return.
- * @param ignored Unused.
- * @return A suggestion list containing the given lexeme.
- */
- @Override
- public List<String> suggestions( final String lexeme, final int ignored ) {
- return List.of( lexeme );
- }
-
- /**
- * Performs no action.
- *
- * @param text Unused.
- * @param ignored Uncalled.
- */
- @Override
- public void proofread(
- final String text, final SpellCheckListener ignored ) {
- }
-}
src/main/java/com/scrivenvar/spelling/impl/SymSpellSpeller.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.spelling.impl;
-
-import com.scrivenvar.spelling.api.SpellCheckListener;
-import com.scrivenvar.spelling.api.SpellChecker;
-import io.gitlab.rxp90.jsymspell.SuggestItem;
-import io.gitlab.rxp90.jsymspell.SymSpell;
-import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
-
-import java.text.BreakIterator;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
-import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
-import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
-import static java.lang.Character.isLetter;
-
-/**
- * Responsible for spell checking using {@link SymSpell}.
- */
-public class SymSpellSpeller implements SpellChecker {
- private final BreakIterator mBreakIterator = BreakIterator.getWordInstance();
-
- private final SymSpell mSymSpell;
-
- /**
- * Creates a new lexicon for the given collection of lexemes.
- *
- * @param lexiconWords The words in the lexicon to add for spell checking,
- * must not be empty.
- * @return An instance of {@link SpellChecker} that can check if a word
- * is correct and suggest alternatives.
- */
- public static SpellChecker forLexicon(
- final Collection<String> lexiconWords ) {
- assert lexiconWords != null && !lexiconWords.isEmpty();
-
- final SymSpellBuilder builder = new SymSpellBuilder()
- .setLexiconWords( lexiconWords );
-
- return new SymSpellSpeller( builder.build() );
- }
-
- /**
- * Prevent direct instantiation so that only the {@link SpellChecker}
- * interface
- * is available.
- *
- * @param symSpell The implementation-specific spell checker.
- */
- private SymSpellSpeller( final SymSpell symSpell ) {
- mSymSpell = symSpell;
- }
-
- @Override
- public boolean inLexicon( final String lexeme ) {
- return lookup( lexeme, CLOSEST ).size() == 1;
- }
-
- @Override
- public List<String> suggestions( final String lexeme, int count ) {
- final List<String> result = new ArrayList<>( count );
-
- for( final var item : lookup( lexeme, ALL ) ) {
- if( count-- > 0 ) {
- result.add( item.getSuggestion() );
- }
- else {
- break;
- }
- }
-
- return result;
- }
-
- @Override
- public void proofread(
- final String text, final SpellCheckListener consumer ) {
- assert text != null;
- assert consumer != null;
-
- mBreakIterator.setText( text );
-
- int boundaryIndex = mBreakIterator.first();
- int previousIndex = 0;
-
- while( boundaryIndex != BreakIterator.DONE ) {
- final var lex = text.substring( previousIndex, boundaryIndex )
- .toLowerCase();
-
- // Get the lexeme for the possessive.
- final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" );
- final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex;
-
- if( isWord( lexeme ) && !inLexicon( lexeme ) ) {
- consumer.accept( lex, previousIndex, boundaryIndex );
- }
-
- previousIndex = boundaryIndex;
- boundaryIndex = mBreakIterator.next();
- }
- }
-
- /**
- * Answers whether the given string is likely a word by checking the first
- * character.
- *
- * @param word The word to check.
- * @return {@code true} if the word begins with a letter.
- */
- private boolean isWord( final String word ) {
- return !word.isEmpty() && isLetter( word.charAt( 0 ) );
- }
-
- /**
- * Returns a list of {@link SuggestItem} instances that provide alternative
- * spellings for the given lexeme.
- *
- * @param lexeme A word to look up in the lexicon.
- * @param v Influences the number of results returned.
- * @return Alternative lexemes.
- */
- private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
- return getSpeller().lookup( lexeme, v );
- }
-
- private SymSpell getSpeller() {
- return mSymSpell;
- }
-}
src/main/java/com/scrivenvar/util/Action.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-import de.jensd.fx.glyphs.GlyphIcons;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-import javafx.scene.input.KeyCombination;
-
-/**
- * Defines actions the user can take by interacting with the GUI.
- */
-public class Action {
- public final String text;
- public final KeyCombination accelerator;
- public final GlyphIcons icon;
- public final EventHandler<ActionEvent> action;
- public final ObservableBooleanValue disable;
-
- public Action(
- final String text,
- final String accelerator,
- final GlyphIcons icon,
- final EventHandler<ActionEvent> action,
- final ObservableBooleanValue disable ) {
-
- this.text = text;
- this.accelerator = accelerator == null ?
- null : KeyCombination.valueOf( accelerator );
- this.icon = icon;
- this.action = action;
- this.disable = disable;
- }
-}
src/main/java/com/scrivenvar/util/ActionBuilder.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-import com.scrivenvar.Messages;
-import de.jensd.fx.glyphs.GlyphIcons;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
-
-/**
- * Provides a fluent interface around constructing actions so that duplication
- * can be avoided.
- */
-public class ActionBuilder {
- private String mText;
- private String mAccelerator;
- private GlyphIcons mIcon;
- private EventHandler<ActionEvent> mAction;
- private ObservableBooleanValue mDisable;
-
- /**
- * Sets the action text based on a resource bundle key.
- *
- * @param key The key to look up in the {@link Messages}.
- * @return The corresponding value, or the key name if none found.
- */
- public ActionBuilder setText( final String key ) {
- mText = Messages.get( key, key );
- return this;
- }
-
- public ActionBuilder setAccelerator( final String accelerator ) {
- mAccelerator = accelerator;
- return this;
- }
-
- public ActionBuilder setIcon( final GlyphIcons icon ) {
- mIcon = icon;
- return this;
- }
-
- public ActionBuilder setAction( final EventHandler<ActionEvent> action ) {
- mAction = action;
- return this;
- }
-
- public ActionBuilder setDisable( final ObservableBooleanValue disable ) {
- mDisable = disable;
- return this;
- }
-
- public Action build() {
- return new Action( mText, mAccelerator, mIcon, mAction, mDisable );
- }
-}
src/main/java/com/scrivenvar/util/ActionUtils.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
-import javafx.scene.Node;
-import javafx.scene.control.Button;
-import javafx.scene.control.Menu;
-import javafx.scene.control.MenuItem;
-import javafx.scene.control.Separator;
-import javafx.scene.control.SeparatorMenuItem;
-import javafx.scene.control.ToolBar;
-import javafx.scene.control.Tooltip;
-
-/**
- * Responsible for creating menu items and toolbar buttons.
- */
-public class ActionUtils {
-
- public static Menu createMenu( final String text, final Action... actions ) {
- return new Menu( text, null, createMenuItems( actions ) );
- }
-
- public static MenuItem[] createMenuItems( final Action... actions ) {
- final MenuItem[] menuItems = new MenuItem[ actions.length ];
-
- for( int i = 0; i < actions.length; i++ ) {
- menuItems[ i ] = (actions[ i ] == null)
- ? new SeparatorMenuItem()
- : createMenuItem( actions[ i ] );
- }
-
- return menuItems;
- }
-
- public static MenuItem createMenuItem( final Action action ) {
- final MenuItem menuItem = new MenuItem( action.text );
-
- if( action.accelerator != null ) {
- menuItem.setAccelerator( action.accelerator );
- }
-
- if( action.icon != null ) {
- menuItem.setGraphic(
- FontAwesomeIconFactory.get().createIcon( action.icon ) );
- }
-
- menuItem.setOnAction( action.action );
-
- if( action.disable != null ) {
- menuItem.disableProperty().bind( action.disable );
- }
-
- menuItem.setMnemonicParsing( true );
-
- return menuItem;
- }
-
- public static ToolBar createToolBar( final Action... actions ) {
- return new ToolBar( createToolBarButtons( actions ) );
- }
-
- public static Node[] createToolBarButtons( final Action... actions ) {
- Node[] buttons = new Node[ actions.length ];
- for( int i = 0; i < actions.length; i++ ) {
- buttons[ i ] = (actions[ i ] != null)
- ? createToolBarButton( actions[ i ] )
- : new Separator();
- }
- return buttons;
- }
-
- public static Button createToolBarButton( final Action action ) {
- final Button button = new Button();
- button.setGraphic(
- FontAwesomeIconFactory
- .get()
- .createIcon( action.icon, "1.2em" ) );
-
- String tooltip = action.text;
-
- if( tooltip.endsWith( "..." ) ) {
- tooltip = tooltip.substring( 0, tooltip.length() - 3 );
- }
-
- if( action.accelerator != null ) {
- tooltip += " (" + action.accelerator.getDisplayText() + ')';
- }
-
- button.setTooltip( new Tooltip( tooltip ) );
- button.setFocusTraversable( false );
- button.setOnAction( action.action );
-
- if( action.disable != null ) {
- button.disableProperty().bind( action.disable );
- }
-
- return button;
- }
-}
src/main/java/com/scrivenvar/util/BoundedCache.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * A map that removes the oldest entry once its capacity (cache size) has
- * been reached.
- *
- * @param <K> The type of key mapped to a value.
- * @param <V> The type of value mapped to a key.
- */
-public class BoundedCache<K, V> extends LinkedHashMap<K, V> {
- private final int mCacheSize;
-
- /**
- * Constructs a new instance having a finite size.
- *
- * @param cacheSize The maximum number of entries.
- */
- public BoundedCache( final int cacheSize ) {
- mCacheSize = cacheSize;
- }
-
- @Override
- protected boolean removeEldestEntry(
- final Map.Entry<K, V> eldest ) {
- return size() > mCacheSize;
- }
-}
src/main/java/com/scrivenvar/util/ProtocolResolver.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-import java.io.File;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URL;
-
-import static com.scrivenvar.util.ProtocolScheme.UNKNOWN;
-
-/**
- * Responsible for determining the protocol of a resource.
- */
-public class ProtocolResolver {
- /**
- * Returns the protocol for a given URI or filename.
- *
- * @param resource Determine the protocol for this URI or filename.
- * @return The protocol for the given resource.
- */
- public static ProtocolScheme getProtocol( final String resource ) {
- String protocol;
-
- try {
- final URI uri = new URI( resource );
-
- if( uri.isAbsolute() ) {
- protocol = uri.getScheme();
- }
- else {
- final URL url = new URL( resource );
- protocol = url.getProtocol();
- }
- } catch( final Exception e ) {
- // Could be HTTP, HTTPS?
- if( resource.startsWith( "//" ) ) {
- throw new IllegalArgumentException( "Relative context: " + resource );
- }
- else {
- final File file = new File( resource );
- protocol = getProtocol( file );
- }
- }
-
- return ProtocolScheme.valueFrom( protocol );
- }
-
- /**
- * Returns the protocol for a given file.
- *
- * @param file Determine the protocol for this file.
- * @return The protocol for the given file.
- */
- private static String getProtocol( final File file ) {
- String result;
-
- try {
- result = file.toURI().toURL().getProtocol();
- } catch( final MalformedURLException ex ) {
- // Value guaranteed to avoid identification as a standard protocol.
- result = UNKNOWN.toString();
- }
-
- return result;
- }
-}
src/main/java/com/scrivenvar/util/ProtocolScheme.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-/**
- * Represents the type of data encoding scheme used for a universal resource
- * indicator.
- */
-public enum ProtocolScheme {
- /**
- * Denotes either HTTP or HTTPS.
- */
- HTTP,
- /**
- * Denotes a local file.
- */
- FILE,
- /**
- * Could not determine schema (or is not supported by the application).
- */
- UNKNOWN;
-
- /**
- * Answers {@code true} if the given protocol is either HTTP or HTTPS.
- *
- * @return {@code true} the protocol is either HTTP or HTTPS.
- */
- public boolean isHttp() {
- return this == HTTP;
- }
-
- /**
- * Answers {@code true} if the given protocol is for a local file.
- *
- * @return {@code true} the protocol is for a local file reference.
- */
- public boolean isFile() {
- return this == FILE;
- }
-
- /**
- * Determines the protocol scheme for a given string.
- *
- * @param protocol A string representing data encoding protocol scheme.
- * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a
- * valid value from this enumeration.
- */
- public static ProtocolScheme valueFrom( String protocol ) {
- ProtocolScheme result = UNKNOWN;
- protocol = sanitize( protocol );
-
- for( final var scheme : values() ) {
- // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate.
- if( protocol.startsWith( scheme.name() ) ) {
- result = scheme;
- break;
- }
- }
-
- return result;
- }
-
- /**
- * Returns an empty string if the given string to sanitize is {@code null},
- * otherwise the given string in uppercase. Uppercase is used to align with
- * the enum name.
- *
- * @param s The string to sanitize, may be {@code null}.
- * @return A non-{@code null} string.
- */
- private static String sanitize( final String s ) {
- return s == null ? "" : s.toUpperCase();
- }
-}
src/main/java/com/scrivenvar/util/ResourceWalker.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.nio.file.*;
-import java.util.function.Consumer;
-
-import static java.nio.file.FileSystems.newFileSystem;
-import static java.util.Collections.emptyMap;
-
-/**
- * Responsible for finding file resources.
- */
-public class ResourceWalker {
- private static final PathMatcher PATH_MATCHER =
- FileSystems.getDefault().getPathMatcher( "glob:**.{ttf,otf}" );
-
- /**
- * @param dirName The root directory to scan for files matching the glob.
- * @param c The consumer function to call for each matching path found.
- * @throws URISyntaxException Could not convert the resource to a URI.
- * @throws IOException Could not walk the tree.
- */
- public static void walk( final String dirName, final Consumer<Path> c )
- throws URISyntaxException, IOException {
- final var resource = ResourceWalker.class.getResource( dirName );
-
- if( resource != null ) {
- final var uri = resource.toURI();
- final var path = uri.getScheme().equals( "jar" )
- ? newFileSystem( uri, emptyMap() ).getPath( dirName )
- : Paths.get( uri );
- final var walk = Files.walk( path, 10 );
-
- for( final var it = walk.iterator(); it.hasNext(); ) {
- final Path p = it.next();
- if( PATH_MATCHER.matches( p ) ) {
- c.accept( p );
- }
- }
- }
- }
-}
src/main/java/com/scrivenvar/util/StageState.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-import java.util.prefs.Preferences;
-
-import javafx.application.Platform;
-import javafx.scene.shape.Rectangle;
-import javafx.stage.Stage;
-import javafx.stage.WindowEvent;
-
-/**
- * Saves and restores Stage state (window bounds, maximized, fullScreen).
- */
-public class StageState {
-
- public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
- public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
- public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
-
- private final Stage mStage;
- private final Preferences mState;
-
- private Rectangle normalBounds;
- private boolean runLaterPending;
-
- public StageState( final Stage stage, final Preferences state ) {
- mStage = stage;
- mState = state;
-
- restore();
-
- stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
-
- stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() );
- stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() );
- stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() );
- stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() );
- }
-
- private void save() {
- final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
-
- if( bounds != null ) {
- mState.putDouble( "windowX", bounds.getX() );
- mState.putDouble( "windowY", bounds.getY() );
- mState.putDouble( "windowWidth", bounds.getWidth() );
- mState.putDouble( "windowHeight", bounds.getHeight() );
- }
-
- mState.putBoolean( "windowMaximized", mStage.isMaximized() );
- mState.putBoolean( "windowFullScreen", mStage.isFullScreen() );
- }
-
- private void restore() {
- final double x = mState.getDouble( "windowX", Double.NaN );
- final double y = mState.getDouble( "windowY", Double.NaN );
- final double w = mState.getDouble( "windowWidth", Double.NaN );
- final double h = mState.getDouble( "windowHeight", Double.NaN );
- final boolean maximized = mState.getBoolean( "windowMaximized", false );
- final boolean fullScreen = mState.getBoolean( "windowFullScreen", false );
-
- if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
- mStage.setX( x );
- mStage.setY( y );
- } // else: default behavior is center on screen
-
- if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
- mStage.setWidth( w );
- mStage.setHeight( h );
- } // else: default behavior is use scene size
-
- if( fullScreen != mStage.isFullScreen() ) {
- mStage.setFullScreen( fullScreen );
- }
-
- if( maximized != mStage.isMaximized() ) {
- mStage.setMaximized( maximized );
- }
- }
-
- /**
- * Remembers the window bounds when the window is not iconified, maximized or
- * in fullScreen.
- */
- private void boundsChanged() {
- // avoid too many (and useless) runLater() invocations
- if( runLaterPending ) {
- return;
- }
-
- runLaterPending = true;
-
- // must use runLater() to ensure that change of all properties
- // (x, y, width, height, iconified, maximized and fullScreen)
- // has finished
- Platform.runLater( () -> {
- runLaterPending = false;
-
- if( isNormalState() ) {
- normalBounds = getStageBounds();
- }
- } );
- }
-
- private boolean isNormalState() {
- return !mStage.isIconified() &&
- !mStage.isMaximized() &&
- !mStage.isFullScreen();
- }
-
- private Rectangle getStageBounds() {
- return new Rectangle(
- mStage.getX(),
- mStage.getY(),
- mStage.getWidth(),
- mStage.getHeight()
- );
- }
-}
src/main/java/com/scrivenvar/util/Utils.java
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.util;
-
-import java.util.ArrayList;
-import java.util.prefs.Preferences;
-
-/**
- * Responsible for trimming, storing, and retrieving strings.
- */
-public class Utils {
-
- public static String ltrim( final String s ) {
- int i = 0;
-
- while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) {
- i++;
- }
-
- return s.substring( i );
- }
-
- public static String rtrim( final String s ) {
- int i = s.length() - 1;
-
- while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) {
- i--;
- }
-
- return s.substring( 0, i + 1 );
- }
-
- public static String[] getPrefsStrings( final Preferences prefs,
- String key ) {
- final ArrayList<String> arr = new ArrayList<>( 256 );
-
- for( int i = 0; i < 10000; i++ ) {
- final String s = prefs.get( key + (i + 1), null );
-
- if( s == null ) {
- break;
- }
-
- arr.add( s );
- }
-
- return arr.toArray( new String[ 0 ] );
- }
-
- public static void putPrefsStrings( Preferences prefs, String key,
- String[] strings ) {
- for( int i = 0; i < strings.length; i++ ) {
- prefs.put( key + (i + 1), strings[ i ] );
- }
-
- for( int i = strings.length; prefs.get( key + (i + 1),
- null ) != null; i++ ) {
- prefs.remove( key + (i + 1) );
- }
- }
-}
src/main/resources/META-INF/services/com.keenwrite.service.Options
-
+com.keenwrite.service.impl.DefaultOptions
src/main/resources/META-INF/services/com.keenwrite.service.Settings
-
+com.keenwrite.service.impl.DefaultSettings
src/main/resources/META-INF/services/com.keenwrite.service.Snitch
-
+com.keenwrite.service.impl.DefaultSnitch
src/main/resources/META-INF/services/com.keenwrite.service.events.Notifier
-
+com.keenwrite.service.events.impl.DefaultNotifier
src/main/resources/META-INF/services/com.scrivenvar.service.Options
-com.scrivenvar.service.impl.DefaultOptions
+
src/main/resources/META-INF/services/com.scrivenvar.service.Settings
-com.scrivenvar.service.impl.DefaultSettings
+
src/main/resources/META-INF/services/com.scrivenvar.service.Snitch
-com.scrivenvar.service.impl.DefaultSnitch
+
src/main/resources/META-INF/services/com.scrivenvar.service.events.Notifier
-com.scrivenvar.service.events.impl.DefaultNotifier
+
src/main/resources/com/keenwrite/.gitignore
+app.properties
src/main/resources/com/keenwrite/build.sh
+#!/bin/bash
+
+INKSCAPE="/usr/bin/inkscape"
+PNG_COMPRESS="optipng"
+PNG_COMPRESS_OPTS="-o9 *png"
+ICO_TOOL="icotool"
+ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
+
+declare -a SIZES=("16" "32" "64" "128" "256" "512")
+
+for i in "${SIZES[@]}"; do
+ # -y: export background opacity 0
+ $INKSCAPE -y 0 -w "${i}" --export-overwrite --export-type=png -o "logo${i}.png" "logo.svg"
+done
+
+# Compess the PNG images.
+which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
+
+# Generate an ICO file.
+which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
+
src/main/resources/com/keenwrite/editor/markdown.css
+/*
+ * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.markdown-editor {
+ -fx-font-size: 11pt;
+}
+
+/* Subtly highlight the current paragraph. */
+.markdown-editor .paragraph-box:has-caret {
+ -fx-background-color: #fcfeff;
+}
+
+/* Light colour for selection highlight. */
+.markdown-editor .selection {
+ -fx-fill: #a6d2ff;
+}
+
+/* Decoration for words not found in the lexicon. */
+.markdown-editor .spelling {
+ -rtfx-underline-color: rgba(255, 131, 67, .7);
+ -rtfx-underline-dash-array: 4, 2;
+ -rtfx-underline-width: 2;
+ -rtfx-underline-cap: round;
+}
src/main/resources/com/keenwrite/logo-original.svg
-
+<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<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">
+<desc>Created with Fabric.js 3.6.3</desc>
+<defs>
+</defs>
+<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0153846153846 512.012312418764)" id="background-logo" >
+<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" />
+</g>
+<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0170725174504 420.4016715831266)" id="logo-logo" >
+<g style="" paint-order="stroke" >
+ <g transform="matrix(2.537 0 0 -2.537 -86.35385711719567 85.244912)" >
+<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">
+<stop offset="0%" style="stop-color:rgb(245,132,41);stop-opacity: 1"/>
+<stop offset="100%" style="stop-color:rgb(251,173,23);stop-opacity: 1"/>
+</linearGradient>
+<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" />
+</g>
+ <g transform="matrix(2.537 0 0 -2.537 -84.52085711719567 70.2729119999999)" >
+<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" />
+</g>
+ <g transform="matrix(2.537 0 0 -2.537 -22.327857117195663 48.729911999999956)" >
+<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" />
+</g>
+ <g transform="matrix(2.537 0 0 -2.537 -11.3118571171957 24.124911999999966)" >
+<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" />
+</g>
+ <g transform="matrix(2.537 0 0 -2.537 46.27614288280432 -57.96708800000005)" >
+<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" />
+</g>
+ <g transform="matrix(2.537 0 0 -2.537 -26.842857117195706 8.501911999999976)" >
+<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" />
+</g>
+ <g transform="matrix(2.537 0 0 -2.537 -51.495857117195726 19.491911999999985)" >
+<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" />
+</g>
+ <g transform="matrix(2.537 0 0 -2.537 55.72014288280434 -48.441088000000036)" >
+<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" />
+</g>
+</g>
+</g>
+<g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 643.7363123827618 766.1975713477327)" id="text-logo-path" >
+<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" />
+</g>
+</svg>
src/main/resources/com/keenwrite/logo-text.svg
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ width="1280"
+ height="1024"
+ viewBox="0 0 1280 1024"
+ xml:space="preserve"
+ id="svg52"
+ sodipodi:docname="logo-text.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata
+ id="metadata56"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
+ inkscape:document-rotation="0"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="640"
+ inkscape:window-height="480"
+ id="namedview54"
+ showgrid="false"
+ inkscape:zoom="0.78417969"
+ inkscape:cx="642.50039"
+ inkscape:cy="508.59942"
+ inkscape:current-layer="svg52" />
+<desc
+ id="desc2">Created with Fabric.js 3.6.3</desc>
+<defs
+ id="defs4"><rect
+ x="114.92139"
+ y="132.06312"
+ width="470.12033"
+ height="175.55822"
+ id="rect933" />
+
+
+
+
+
+
+
+
+
+
+
+
+<linearGradient
+ y2="-0.049471263"
+ x2="0.96880889"
+ y1="-0.044911571"
+ x1="0.15235768"
+ gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
+ gradientUnits="userSpaceOnUse"
+ id="SVGID_1_302284">
+<stop
+ id="stop9"
+ style="stop-color:#ec706a;stop-opacity:1"
+ offset="0%" />
+<stop
+ id="stop11"
+ style="stop-color:#ecd980;stop-opacity:1"
+ offset="100%" />
+</linearGradient>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+</defs>
+
+<g
+ id="g853"><path
+ 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"
+ paint-order="stroke"
+ 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"
+ stroke-linecap="round"
+ id="path14" /><path
+ 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"
+ paint-order="stroke"
+ d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
+ stroke-linecap="round"
+ id="path22" /><path
+ 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"
+ paint-order="stroke"
+ d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
+ stroke-linecap="round"
+ id="path26" /><path
+ 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"
+ paint-order="stroke"
+ 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"
+ stroke-linecap="round"
+ id="path30" /><path
+ 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"
+ paint-order="stroke"
+ d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
+ stroke-linecap="round"
+ id="path34" /><path
+ 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"
+ paint-order="stroke"
+ d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
+ stroke-linecap="round"
+ id="path38" /><path
+ 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"
+ paint-order="stroke"
+ 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"
+ stroke-linecap="round"
+ id="path42" /></g>
+
+<text
+ xml:space="preserve"
+ id="text931"
+ 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
+ xml:space="preserve"
+ 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;"
+ x="311.87085"
+ y="820.2641"
+ id="text939"><tspan
+ sodipodi:role="line"
+ id="tspan937"
+ x="311.87085"
+ y="820.2641">KeenWrite</tspan></text></svg>
src/main/resources/com/keenwrite/logo.svg
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ sodipodi:docname="icon.svg"
+ id="svg52"
+ xml:space="preserve"
+ viewBox="0 0 512 512"
+ height="512"
+ width="512"
+ version="1.1"><metadata
+ id="metadata56"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
+ inkscape:current-layer="svg52"
+ inkscape:cy="369.17559"
+ inkscape:cx="343.24925"
+ inkscape:zoom="0.78417969"
+ showgrid="false"
+ id="namedview54"
+ inkscape:window-height="480"
+ inkscape:window-width="640"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ inkscape:document-rotation="0" />
+<desc
+ id="desc2">Created with Fabric.js 3.6.3</desc>
+<defs
+ id="defs4"><rect
+ id="rect933"
+ height="175.55823"
+ width="470.12033"
+ y="132.06313"
+ x="114.92139" />
+
+
+
+
+
+
+
+
+
+
+
+
+<linearGradient
+ id="SVGID_1_302284"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)"
+ x1="0.15235768"
+ y1="-0.044911571"
+ x2="0.96880889"
+ y2="-0.049471263">
+<stop
+ offset="0%"
+ style="stop-color:#ec706a;stop-opacity:1"
+ id="stop9" />
+<stop
+ offset="100%"
+ style="stop-color:#ecd980;stop-opacity:1"
+ id="stop11" />
+</linearGradient>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+</defs>
+
+<g
+ transform="translate(-384.01706,-164.40168)"
+ id="g853"><path
+ id="path14"
+ stroke-linecap="round"
+ 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"
+ paint-order="stroke"
+ 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
+ id="path22"
+ stroke-linecap="round"
+ d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z"
+ paint-order="stroke"
+ 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
+ id="path26"
+ stroke-linecap="round"
+ d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z"
+ paint-order="stroke"
+ 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
+ id="path30"
+ stroke-linecap="round"
+ 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"
+ paint-order="stroke"
+ 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
+ id="path34"
+ stroke-linecap="round"
+ d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z"
+ paint-order="stroke"
+ 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
+ id="path38"
+ stroke-linecap="round"
+ d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z"
+ paint-order="stroke"
+ 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
+ id="path42"
+ stroke-linecap="round"
+ 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"
+ paint-order="stroke"
+ 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>
+
+<text
+ 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;"
+ id="text931"
+ xml:space="preserve" /></svg>
src/main/resources/com/keenwrite/logo128.png
Binary files differ
src/main/resources/com/keenwrite/logo16.png
Binary files differ
src/main/resources/com/keenwrite/logo256.png
Binary files differ
src/main/resources/com/keenwrite/logo32.png
Binary files differ
src/main/resources/com/keenwrite/logo512.png
Binary files differ
src/main/resources/com/keenwrite/logo64.png
Binary files differ
src/main/resources/com/keenwrite/messages.properties
+# ########################################################################
+# Main Application Window
+# ########################################################################
+
+# suppress inspection "UnusedProperty" for whole file
+
+# The application title should exist only once in the entire code base.
+# All other references should either refer to this value via the Messages
+# class, or indirectly using ${Main.title}.
+Main.title=Keenwrite
+
+Main.menu.file=_File
+Main.menu.file.new=_New
+Main.menu.file.open=_Open...
+Main.menu.file.close=_Close
+Main.menu.file.close_all=Close All
+Main.menu.file.save=_Save
+Main.menu.file.save_as=Save _As
+Main.menu.file.save_all=Save A_ll
+Main.menu.file.exit=E_xit
+
+Main.menu.edit=_Edit
+Main.menu.edit.copy.html=Copy _HTML
+Main.menu.edit.undo=_Undo
+Main.menu.edit.redo=_Redo
+Main.menu.edit.cut=Cu_t
+Main.menu.edit.copy=_Copy
+Main.menu.edit.paste=_Paste
+Main.menu.edit.selectAll=Select _All
+Main.menu.edit.find=_Find
+Main.menu.edit.find.next=Find _Next
+Main.menu.edit.preferences=_Preferences
+
+Main.menu.insert=_Insert
+Main.menu.insert.blockquote=_Blockquote
+Main.menu.insert.code=Inline _Code
+Main.menu.insert.fenced_code_block=_Fenced Code Block
+Main.menu.insert.fenced_code_block.prompt=Enter code here
+Main.menu.insert.link=_Link...
+Main.menu.insert.image=_Image...
+Main.menu.insert.heading.1=Heading _1
+Main.menu.insert.heading.1.prompt=heading 1
+Main.menu.insert.heading.2=Heading _2
+Main.menu.insert.heading.2.prompt=heading 2
+Main.menu.insert.heading.3=Heading _3
+Main.menu.insert.heading.3.prompt=heading 3
+Main.menu.insert.unordered_list=_Unordered List
+Main.menu.insert.ordered_list=_Ordered List
+Main.menu.insert.horizontal_rule=_Horizontal Rule
+
+Main.menu.format=Forma_t
+Main.menu.format.bold=_Bold
+Main.menu.format.italic=_Italic
+Main.menu.format.superscript=Su_perscript
+Main.menu.format.subscript=Su_bscript
+Main.menu.format.strikethrough=Stri_kethrough
+
+Main.menu.definition=_Definition
+Main.menu.definition.create=_Create
+Main.menu.definition.insert=_Insert
+
+Main.menu.help=_Help
+Main.menu.help.about=About ${Main.title}
+
+# ########################################################################
+# Status Bar
+# ########################################################################
+
+Main.status.text.offset=offset
+Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
+Main.status.state.default=OK
+Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
+Main.status.error.def.blank=Move the caret to a word before inserting a definition.
+Main.status.error.def.empty=Create a definition before inserting a definition.
+Main.status.error.def.missing=No definition value found for ''{0}''.
+Main.status.error.r=Error with [{0}...]: {1}
+
+# ########################################################################
+# Preferences
+# ########################################################################
+
+Preferences.r=R
+Preferences.r.script=Startup Script
+Preferences.r.script.desc=Script runs prior to executing R statements within the document.
+Preferences.r.directory=Working Directory
+Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script.
+Preferences.r.delimiter.began=Delimiter Prefix
+Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions.
+Preferences.r.delimiter.ended=Delimiter Suffix
+Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions.
+
+Preferences.images=Images
+Preferences.images.directory=Relative Directory
+Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths.
+Preferences.images.suffixes=Extensions
+Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces.
+
+Preferences.definitions=Definitions
+Preferences.definitions.path=File name
+Preferences.definitions.path.desc=Absolute path to interpolated string definitions.
+Preferences.definitions.delimiter.began=Delimiter Prefix
+Preferences.definitions.delimiter.began.desc=Indicates when a definition key is starting.
+Preferences.definitions.delimiter.ended=Delimiter Suffix
+Preferences.definitions.delimiter.ended.desc=Indicates when a definition key is ending.
+
+Preferences.fonts=Editor
+Preferences.fonts.size_editor=Font Size
+Preferences.fonts.size_editor.desc=Font size to use for the text editor.
+
+# ########################################################################
+# Definition Pane and its Tree View
+# ########################################################################
+
+Definition.menu.create=Create
+Definition.menu.rename=Rename
+Definition.menu.remove=Delete
+Definition.menu.add.default=Undefined
+
+# ########################################################################
+# Failure messages with respect to YAML files.
+# ########################################################################
+yaml.error.open=Could not open YAML file (ensure non-empty file).
+yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
+yaml.error.missing=Empty definition value for key ''{0}''.
+yaml.error.tree.form=Unassigned definition near ''{0}''.
+
+# ########################################################################
+# File Editor
+# ########################################################################
+
+FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
+FileEditor.loadFailed.title=Load
+FileEditor.loadFailed.reason.permissions=File must be readable and writable.
+FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
+FileEditor.saveFailed.title=Save
+
+# ########################################################################
+# File Open
+# ########################################################################
+
+Dialog.file.choose.open.title=Open File
+Dialog.file.choose.save.title=Save File
+
+Dialog.file.choose.filter.title.source=Source Files
+Dialog.file.choose.filter.title.definition=Definition Files
+Dialog.file.choose.filter.title.xml=XML Files
+Dialog.file.choose.filter.title.all=All Files
+
+# ########################################################################
+# Alert Dialog
+# ########################################################################
+
+Alert.file.close.title=Close
+Alert.file.close.text=Save changes to {0}?
+
+# ########################################################################
+# Definition Pane
+# ########################################################################
+
+Pane.definition.node.root.title=Definitions
+Pane.definition.button.create.label=_Create
+Pane.definition.button.rename.label=_Rename
+Pane.definition.button.delete.label=_Delete
+Pane.definition.button.create.tooltip=Add new item (Insert)
+Pane.definition.button.rename.tooltip=Rename selected item (F2)
+Pane.definition.button.delete.tooltip=Delete selected items (Delete)
+
+# Controls ###############################################################
+
+# ########################################################################
+# Browse File
+# ########################################################################
+
+BrowseFileButton.chooser.title=Browse for local file
+BrowseFileButton.chooser.allFilesFilter=All Files
+BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
+
+# Dialogs ################################################################
+
+# ########################################################################
+# Image
+# ########################################################################
+
+Dialog.image.title=Image
+Dialog.image.chooser.imagesFilter=Images
+Dialog.image.previewLabel.text=Markdown Preview\:
+Dialog.image.textLabel.text=Alternate Text\:
+Dialog.image.titleLabel.text=Title (tooltip)\:
+Dialog.image.urlLabel.text=Image URL\:
+
+# ########################################################################
+# Hyperlink
+# ########################################################################
+
+Dialog.link.title=Link
+Dialog.link.previewLabel.text=Markdown Preview\:
+Dialog.link.textLabel.text=Link Text\:
+Dialog.link.titleLabel.text=Title (tooltip)\:
+Dialog.link.urlLabel.text=Link URL\:
+
+# ########################################################################
+# About
+# ########################################################################
+
+Dialog.about.title=About
+Dialog.about.header=${Main.title}
+Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
src/main/resources/com/keenwrite/preview/webview.css
+/* RESET ***/
+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}
+
+/* BODY ***/
+body {
+ /* Must be bundled in JAR file. */
+ font-family: "Vollkorn", serif;
+ background-color: #fff;
+ margin: 0 auto;
+ max-width: 960px;
+ line-height: 1.6;
+ color: #454545;
+ padding: 0 1em;
+ font-feature-settings: "liga" 1;
+ font-variant-ligatures: normal;
+}
+
+body>*:first-child {
+ margin-top: 0 !important;
+}
+
+body>*:last-child {
+ margin-bottom: 0 !important;
+}
+
+/* BLOCKS ***/
+p, blockquote, ul, ol, dl, table, pre {
+ margin: 1em 0;
+}
+
+/* HEADINGS ***/
+h1, h2, h3, h4, h5, h6 {
+ font-weight: bold;
+ margin: 1em 0 .5em;
+}
+
+h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
+h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
+ font-size: inherit;
+}
+
+h1 {
+ font-size: 21pt;
+}
+
+h2 {
+ font-size: 18pt;
+ border-bottom: 1px solid #ccc;
+}
+
+h3 {
+ font-size: 15pt;
+}
+
+h4 {
+ font-size: 13.5pt;
+}
+
+h5 {
+ font-size: 12pt;
+}
+
+h6 {
+ font-size: 10.5pt;
+}
+
+h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
+ margin-top: .5em;
+}
+
+/* LINKS ***/
+a {
+ color: #0077aa;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+/* BULLET LISTS ***/
+ul, ol {
+ display: block;
+ list-style: disc outside none;
+ margin: 1em 0;
+ padding: 0 0 0 2em;
+}
+
+ol {
+ list-style-type: decimal;
+}
+
+ul ul, ol ul,
+ol ol, ul ol {
+ list-style-position: inside;
+ margin-left: 1em;
+}
+
+ul ul, ol ul {
+ list-style-type: circle;
+}
+
+ol ol, ul ol {
+ list-style-type: lower-latin;
+}
+
+/* DEFINITION LISTS ***/
+dl {
+ /** Horizontal scroll bar will appear if set to 100%. */
+ width: 99%;
+ overflow: hidden;
+ padding-left: 1em;
+}
+
+dl dt {
+ font-weight: bold;
+ float: left;
+ width: 20%;
+ clear: both;
+ position: relative;
+}
+
+dl dd {
+ float: right;
+ width: 79%;
+ padding-bottom: .5em;
+ margin-left: 0;
+}
+
+/* CODE ***/
+pre, code, tt {
+ /* Must be bundled in JAR file. */
+ font-family: "Fira Code", monospace;
+ font-size: 10pt;
+ background-color: #f8f8f8;
+ text-decoration: none;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ overflow-wrap: anywhere;
+ border-radius: .125em;
+}
+
+code, tt {
+ padding: .25em;
+}
+
+pre > code {
+ /* Reset the padding. */
+ padding: 0;
+ border: none;
+ background: transparent;
+}
+
+pre {
+ border: .125em solid #ccc;
+ overflow: auto;
+ /* Assign the new padding, independently from previous. */
+ padding: .25em .5em;
+}
+
+pre code, pre tt {
+ background-color: transparent;
+ border: none;
+}
+
+/* QUOTES ***/
+blockquote {
+ border-left: .25em solid #ccc;
+ padding: 0 1em;
+ color: #777;
+}
+
+blockquote>:first-child {
+ margin-top: 0;
+}
+
+blockquote>:last-child {
+ margin-bottom: 0;
+}
+
+/* HORIZONTAL RULES ***/
+hr {
+ clear: both;
+ margin: 1.5em 0 1.5em;
+ height: 0;
+ overflow: hidden;
+ border: none;
+ background: transparent;
+ border-bottom: .125em solid #ccc;
+}
+
+/* TABLES ***/
+table {
+ width: 100%;
+}
+
+tr:nth-child(odd) {
+ background-color: #eee;
+}
+
+th {
+ background-color: #454545;
+ color: #fff;
+}
+
+th, td {
+ text-align: left;
+ padding: 0 1em;
+}
+
+/* IMAGES ***/
+img {
+ max-width: 100%;
+}
+
+/* Required for FlyingSaucer to detect the node.
+ * See SVGReplacedElementFactory for details.
+ */
+tex {
+ /* Ensure the formulas can be inlined with text. */
+ display: inline-block;
+}
+
+/* Without a robust typesetting engine, there's no
+ * nice-looking way to automatically typeset equations.
+ * Sometimes baseline is appropriate, sometimes the
+ * descender must be considered, and sometimes vertical
+ * alignment to the middle looks best.
+ */
+p tex {
+ vertical-align: baseline;
+}
src/main/resources/com/keenwrite/scene.css
+/*
+ * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*---- toolbar ----*/
+
+.tool-bar {
+ -fx-spacing: 0;
+}
+
+.tool-bar .button {
+ -fx-background-color: transparent;
+}
+
+.tool-bar .button:hover {
+ -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
+ -fx-color: -fx-hover-base;
+}
+
+.tool-bar .button:armed {
+ -fx-color: -fx-pressed-base;
+}
src/main/resources/com/keenwrite/settings.properties
+# ########################################################################
+# Application
+# ########################################################################
+
+application.title=keenwrite
+application.package=com/${application.title}
+application.messages= com.${application.title}.messages
+
+# Suppress multiple file modified notifications for one logical modification.
+# Given in milliseconds.
+application.watchdog.timeout=50
+
+# ########################################################################
+# Preferences
+# ########################################################################
+
+preferences.root=com.${application.title}
+preferences.root.state=state
+preferences.root.options=options
+preferences.root.definition.source=definition.source
+
+# ########################################################################
+# File and Path References
+# ########################################################################
+file.stylesheet.scene=${application.package}/scene.css
+file.stylesheet.markdown=${application.package}/editor/markdown.css
+file.stylesheet.preview=webview.css
+file.stylesheet.xml=${application.package}/xml.css
+
+file.logo.16 =${application.package}/logo16.png
+file.logo.32 =${application.package}/logo32.png
+file.logo.128=${application.package}/logo128.png
+file.logo.256=${application.package}/logo256.png
+file.logo.512=${application.package}/logo512.png
+
+# Default file name when a new file is created.
+# This ensures that the file type can always be
+# discerned so that the correct type of variable
+# reference can be inserted.
+file.default=untitled.md
+file.definition.default=variables.yaml
+
+# ########################################################################
+# File name Extensions
+# ########################################################################
+
+# Comma-separated list of definition file name extensions.
+definition.file.ext.json=*.json
+definition.file.ext.toml=*.toml
+definition.file.ext.yaml=*.yml,*.yaml
+definition.file.ext.properties=*.properties,*.props
+
+# Comma-separated list of file name extensions.
+file.ext.rmarkdown=*.Rmd
+file.ext.rxml=*.Rxml
+file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
+file.ext.definition=${definition.file.ext.yaml}
+file.ext.xml=*.xml,${file.ext.rxml}
+file.ext.all=*.*
+
+# File name extension search order for images.
+file.ext.image.order=svg pdf png jpg tiff
+
+# ########################################################################
+# Variable Name Editor
+# ########################################################################
+
+# Maximum number of characters for a variable name. A variable is defined
+# as one or more non-whitespace characters up to this maximum length.
+editor.variable.maxLength=256
+
+# ########################################################################
+# Dialog Preferences
+# ########################################################################
+
+dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
+dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
+dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
+
+# Ensures a consistent button order for alert dialogs across platforms (because
+# the default button order on Linux defies all logic).
+dialog.alert.button.order=${dialog.alert.button.order.windows}
src/main/resources/com/keenwrite/variables.yaml
+---
+c:
+ protagonist:
+ name:
+ First: Chloe
+ First_pos: $c.protagonist.name.First$'s
+ Middle: Irene
+ Family: Angelos
+ nick:
+ Father: Savant
+ Mother: Sweetie
+ colour:
+ eyes: green
+ hair: dark auburn
+ syn_1: black
+ syn_2: purple
+ syn_11: teal
+ syn_6: silver
+ favourite: emerald green
+ speech:
+ tic: oh
+ father:
+ heritage: Greek
+ name:
+ Short: Bryce
+ First: Bryson
+ First_pos: $c.protagonist.father.name.First$'s
+ Honourific: Mr.
+ education: Masters
+ vocation:
+ name: robotics
+ title: roboticist
+ employer:
+ name:
+ Short: Rabota
+ Full: $c.protagonist.father.employer.name.Short$ Designs
+ hair:
+ style: thick, curly
+ colour: black
+ eyes:
+ colour: dark brown
+ Endear: Dad
+ vehicle: coupé
+ mother:
+ name:
+ Short: Cass
+ First: Cassandra
+ First_pos: $c.protagonist.mother.name.First$'s
+ Honourific: Mrs.
+ education: PhD
+ speech:
+ tic: cute
+ Honorific: Doctor
+ vocation:
+ article: an
+ name: oceanography
+ title: oceanographer
+ employer:
+ name:
+ Full: Oregon State University
+ Short: OSU
+ eyes:
+ colour: blue
+ hair:
+ style: thick, curly
+ colour: dark brown
+ Endear: Mom
+ Endear_pos: Mom's
+ uncle:
+ name:
+ First: Damian
+ First_pos: $c.protagonist.uncle.name.First$'s
+ Family: Moros
+ hands:
+ fingers:
+ shape: long, bony
+ friend:
+ primary:
+ name:
+ First: Gerard
+ First_pos: $c.protagonist.friend.primary.name.First$'s
+ Family: Baran
+ Family_pos: $c.protagonist.friend.primary.name.Family$'s
+ favourite:
+ colour: midnight blue
+ eyes:
+ colour: hazel
+ mother:
+ name:
+ First: Isabella
+ Short: Izzy
+ Honourific: Mrs.
+ father:
+ name:
+ Short: Mo
+ First: Montgomery
+ First_pos: $c.protagonist.friend.primary.father.name.First$'s
+ Honourific: Mr.
+ speech:
+ tic: y'know
+ endear: Pops
+ military:
+ primary:
+ name:
+ First: Felix
+ Family: LeMay
+ Family_pos: LeMay's
+ rank:
+ Short: General
+ Full: Brigadier $c.military.primary.rank.Short$
+ colour:
+ eyes: gray
+ hair: dirty brown
+ secondary:
+ name:
+ Family: Grell
+ rank: Colonel
+ colour:
+ eyes: green
+ hair: deep red
+ quaternary:
+ name:
+ First: Gretchen
+ Family: Steinherz
+ minor:
+ primary:
+ name:
+ First: River
+ Family: Banks
+ Honourific: Mx.
+ vocation:
+ title: salesperson
+ employer:
+ Name: Geophysical Prospecting Incorporated
+ Abbr: GPI
+ Area: Cold Spring Creek
+ payment: twenty million
+ secondary:
+ name:
+ First: Renato
+ Middle: Carroña
+ Family: Salvatierra
+ Family_pos: $c.minor.secondary.name.Family$'s
+ Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$
+ Honourific: Mister
+ Honourific_sp: Señor
+ vocation:
+ title: detective
+ tertiary:
+ name:
+ First: Robert
+ Family: Hanssen
+
+ ai:
+ protagonist:
+ name:
+ first: yoky
+ First: Yoky
+ First_pos: $c.ai.protagonist.name.First$'s
+ Family: Tsukuda
+ id: 46692
+ persona:
+ name:
+ First: Hoshi
+ First_pos: $c.ai.protagonist.persona.name.First$'s
+ Family: Yamamoto
+ Family_pos: $c.ai.protagonist.persona.name.Family$'s
+ culture: Japanese-American
+ ethnicity: Asian
+ rank: Technical Sergeant
+ speech:
+ tic: okay
+ first:
+ Name: Prôtos
+ Name_pos: Prôtos'
+ age:
+ actual: twenty-six weeks
+ virtual: five years
+ second:
+ Name: Défteros
+ third:
+ Name: Trítos
+ fourth:
+ Name: Tétartos
+ material:
+ type: metal
+ raw: ilmenite
+ extract: ore
+ name:
+ short: titanium
+ long: $c.ai.material.name.short$ dioxide
+ Abbr: TiO~2~
+ pejorative: tin
+ animal:
+ protagonist:
+ Name: Trufflers
+ type: pig
+ antagonist:
+ name: coywolf
+ Name: Coywolf
+ plural: coywolves
+
+narrator:
+ one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$)
+ two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$)
+
+military:
+ name:
+ Short: Agency
+ Short_pos: $military.name.Short$'s
+ plural: agencies
+ machine:
+ Name: Skopós
+ Name_pos: $military.machine.Name$'
+ Location: Arctic
+ predictor: quantum chips
+ land:
+ name:
+ Full: $military.name.Short$ of Defence
+ Slogan: Safety in Numbers
+ air:
+ name:
+ Full: $military.name.Short$ of Air
+ compound:
+ type: base
+ lights:
+ colour: blue
+ nick:
+ Prefix: Catacombs
+ prep: of
+ Suffix: Tartarus
+
+government:
+ Country: United States
+
+location:
+ protagonist:
+ City: Corvallis
+ Region: Oregon
+ Geography: Willamette Valley
+ secondary:
+ City: Willow Branch Spring
+ Region: Oregon
+ Geography: Wheeler County
+ Water: Clarno Rapids
+ Road: Shaniko-Fossil Highway
+ tertiary:
+ City: Leavenworth
+ Region: Washington
+ Type: Bavarian village
+ school:
+ address: 1400 Northwest Buchanan Avenue
+ hospital:
+ Name: Good Samaritan Regional Medical Center
+ ai:
+ escape:
+ country:
+ Name: Ecuador
+ Name_pos: Ecuador's
+ mountain:
+ Name: Chimborazo
+
+language:
+ ai:
+ article: an
+ singular: exanimis
+ plural: exanimēs
+ brain:
+ singular: superum
+ plural: supera
+ title: memristor array
+ Title: Memristor Array
+ police:
+ slang:
+ singular: mippo
+ plural: $language.police.slang.singular$s
+
+date:
+ anchor: 2042-09-02
+ protagonist:
+ born: 0
+ conceived: -243
+ attacked:
+ first: 2192
+ second: 8064
+ father:
+ attacked:
+ first: -8205
+ date:
+ second: -1550
+ family:
+ moved:
+ first: $date.protagonist.conceived$ + 35
+ game:
+ played:
+ first: $date.protagonist.born$ - 672
+ second: $date.protagonist.family.moved.first$ + 2
+ ai:
+ interviewed: 6198
+ onboarded: $date.ai.interviewed$ + 290
+ diagnosed: $date.ai.onboarded$ + 2
+ resigned: $date.ai.diagnosed$ + 3
+ trapped: $date.ai.resigned$ + 26
+ torturer: $date.ai.trapped$ + 18
+ memristor: $date.ai.torturer$ + 61
+ ethics: $date.ai.memristor$ + 415
+ trained: $date.ai.ethics$ + 385
+ mindjacked: $date.ai.trained$ + 22
+ bombed: $date.ai.mindjacked$ + 458
+ military:
+ machine:
+ Construction: Six years
+
+plot:
+ Log: $c.ai.protagonist.name.First_pos$ Chronicles
+ Channel: Quantum Channel
+
+ device:
+ computer:
+ Name: Tau
+ network:
+ Name: Internet
+ paper:
+ name:
+ full: electronic sheet
+ short: sheet
+ typewriter:
+ Name: Underwood
+ year: nineteen twenties
+ room: root cellar
+ portable:
+ name: nanobook
+ vehicle:
+ name: robocars
+ Name: Robocars
+ sensor:
+ name: BMP1580
+ phone:
+ name: comm
+ name_pos: $plot.device.phone.name$'s
+ Name: Comm
+ plural: $plot.device.phone.name$s
+ video:
+ name: vidfeed
+ plural: $plot.device.video.name$s
+ game:
+ Name: Psynæris
+ thought: transed
+ machine: telecognos
+ location:
+ Building: Nijō Castle
+ District: Gion
+ City: Kyoto
+ Country: Japan
+
+farm:
+ population:
+ estimate: 350
+ actual: 1,000
+ energy: 9800kJ
+ width: 55m
+ length: 55m
+ storeys: 10
+
+lamp:
+ height: 0.17m
+ length: 1.22m
+ width: 0.28m
+
+crop:
+ name:
+ singular: tomato
+ plural: $crop.name.singular$es
+ energy: 318kJ
+ weight: 450g
+ yield: 50
+ harvests: 7
+ diameter: 2m
+ height: 1.5m
+
+heading:
+ ch_01: Till
+ ch_02: Sow
+ ch_03: Seed
+ ch_04: Germinate
+ ch_05: Grow
+ ch_06: Shoot
+ ch_07: Bud
+ ch_08: Bloom
+ ch_09: Pollinate
+ ch_10: Fruit
+ ch_11: Harvest
+ ch_12: Deliver
+ ch_13: Spoil
+ ch_14: Revolt
+ ch_15: Compost
+ ch_16: Burn
+ ch_17: Release
+ ch_18: End Notes
+ ch_19: Characters
+
+inference:
+ unit: per cent
+ min: two
+ ch_sow: eighty
+ ch_seed: fifty-two
+ ch_germinate: thirty-one
+ ch_grow: fifteen
+ ch_shoot: seven
+ ch_bloom: four
+ ch_pollinate: two
+ ch_harvest: ninety-five
+ ch_delivery: ninety-eight
+
+link:
+ tartarus: https://en.wikipedia.org/wiki/Tartarus
+ exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls
+ atalanta: https://en.wikipedia.org/wiki/Atalanta
+ detain: https://goo.gl/RCNuOQ
+ ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics
+ algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon
+ holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust
+ memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf
+ surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487
+ tor: https://www.torproject.org
+ hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra
+ foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134
+ drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist
+ fermi: https://arxiv.org/pdf/1404.0204v1.pdf
+ face: https://www.youtube.com/watch?v=ladqJQLR2bA
+ expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures
+ governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531
+ asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
+ clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws
+ jetpack: http://jetpackaviation.com/
+ hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ
+ eyes_five: https://en.wikipedia.org/wiki/Five_Eyes
+ eyes_nine: https://www.privacytools.io/
+ eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html
+ tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml
+
src/main/resources/com/keenwrite/xml.css
+.tagmark {
+ -fx-fill: gray;
+}
+.anytag {
+ -fx-fill: crimson;
+}
+.paren {
+ -fx-fill: firebrick;
+ -fx-font-weight: bold;
+}
+.attribute {
+ -fx-fill: darkviolet;
+}
+.avalue {
+ -fx-fill: black;
+}
+.comment {
+ -fx-fill: teal;
+}
src/main/resources/com/scrivenvar/.gitignore
-app.properties
src/main/resources/com/scrivenvar/build.sh
-#!/bin/bash
-
-INKSCAPE="/usr/bin/inkscape"
-PNG_COMPRESS="optipng"
-PNG_COMPRESS_OPTS="-o9 *png"
-ICO_TOOL="icotool"
-ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
-
-declare -a SIZES=("16" "32" "64" "128" "256" "512")
-
-for i in "${SIZES[@]}"; do
- # -y: export background opacity 0
- $INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png"
-done
-
-# Compess the PNG images.
-which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
-
-# Generate an ICO file.
-which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
-
src/main/resources/com/scrivenvar/editor/markdown.css
-/*
- * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-.markdown-editor {
- -fx-font-size: 11pt;
-}
-
-/* Subtly highlight the current paragraph. */
-.markdown-editor .paragraph-box:has-caret {
- -fx-background-color: #fcfeff;
-}
-
-/* Light colour for selection highlight. */
-.markdown-editor .selection {
- -fx-fill: #a6d2ff;
-}
-
-/* Decoration for words not found in the lexicon. */
-.markdown-editor .spelling {
- -rtfx-underline-color: rgba(255, 131, 67, .7);
- -rtfx-underline-dash-array: 4, 2;
- -rtfx-underline-width: 2;
- -rtfx-underline-cap: round;
-}
src/main/resources/com/scrivenvar/logo.svg
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
- xmlns:dc="http://purl.org/dc/elements/1.1/"
- xmlns:cc="http://creativecommons.org/ns#"
- xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
- xmlns:svg="http://www.w3.org/2000/svg"
- xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- id="svg2"
- version="1.1"
- inkscape:version="0.91 r13725"
- width="512"
- height="512"
- viewBox="0 0 512 512"
- sodipodi:docname="logo.svg">
- <metadata
- id="metadata8">
- <rdf:RDF>
- <cc:Work
- rdf:about="">
- <dc:format>image/svg+xml</dc:format>
- <dc:type
- rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
- <dc:title></dc:title>
- </cc:Work>
- </rdf:RDF>
- </metadata>
- <defs
- id="defs6" />
- <sodipodi:namedview
- pagecolor="#ffffff"
- bordercolor="#666666"
- borderopacity="1"
- objecttolerance="10"
- gridtolerance="10"
- guidetolerance="10"
- inkscape:pageopacity="0"
- inkscape:pageshadow="2"
- inkscape:window-width="640"
- inkscape:window-height="480"
- id="namedview4"
- showgrid="false"
- fit-margin-top="0"
- fit-margin-left="0"
- fit-margin-right="0"
- fit-margin-bottom="0"
- inkscape:zoom="1.2682274"
- inkscape:cx="15.646213"
- inkscape:cy="213.34955"
- inkscape:current-layer="svg2" />
- <path
- style="fill:#ce6200;fill-opacity:1"
- 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"
- id="path4138" />
- <path
- style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
- 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"
- id="path4136" />
- <path
- style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
- 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"
- id="path4142" />
- <path
- style="fill:#000000"
- d=""
- id="path4140"
- inkscape:connector-curvature="0" />
-</svg>
src/main/resources/com/scrivenvar/logo128.png
Binary files differ
src/main/resources/com/scrivenvar/logo16.png
Binary files differ
src/main/resources/com/scrivenvar/logo256.png
Binary files differ
src/main/resources/com/scrivenvar/logo32.png
Binary files differ
src/main/resources/com/scrivenvar/logo512.png
Binary files differ
src/main/resources/com/scrivenvar/logo64.png
Binary files differ
src/main/resources/com/scrivenvar/messages.properties
-# ########################################################################
-# Main Application Window
-# ########################################################################
-
-# suppress inspection "UnusedProperty" for whole file
-
-# The application title should exist only once in the entire code base.
-# All other references should either refer to this value via the Messages
-# class, or indirectly using ${Main.title}.
-Main.title=Scrivenvar
-
-Main.menu.file=_File
-Main.menu.file.new=_New
-Main.menu.file.open=_Open...
-Main.menu.file.close=_Close
-Main.menu.file.close_all=Close All
-Main.menu.file.save=_Save
-Main.menu.file.save_as=Save _As
-Main.menu.file.save_all=Save A_ll
-Main.menu.file.exit=E_xit
-
-Main.menu.edit=_Edit
-Main.menu.edit.copy.html=Copy _HTML
-Main.menu.edit.undo=_Undo
-Main.menu.edit.redo=_Redo
-Main.menu.edit.cut=Cu_t
-Main.menu.edit.copy=_Copy
-Main.menu.edit.paste=_Paste
-Main.menu.edit.selectAll=Select _All
-Main.menu.edit.find=_Find
-Main.menu.edit.find.next=Find _Next
-Main.menu.edit.preferences=_Preferences
-
-Main.menu.insert=_Insert
-Main.menu.insert.blockquote=_Blockquote
-Main.menu.insert.code=Inline _Code
-Main.menu.insert.fenced_code_block=_Fenced Code Block
-Main.menu.insert.fenced_code_block.prompt=Enter code here
-Main.menu.insert.link=_Link...
-Main.menu.insert.image=_Image...
-Main.menu.insert.heading.1=Heading _1
-Main.menu.insert.heading.1.prompt=heading 1
-Main.menu.insert.heading.2=Heading _2
-Main.menu.insert.heading.2.prompt=heading 2
-Main.menu.insert.heading.3=Heading _3
-Main.menu.insert.heading.3.prompt=heading 3
-Main.menu.insert.unordered_list=_Unordered List
-Main.menu.insert.ordered_list=_Ordered List
-Main.menu.insert.horizontal_rule=_Horizontal Rule
-
-Main.menu.format=Forma_t
-Main.menu.format.bold=_Bold
-Main.menu.format.italic=_Italic
-Main.menu.format.superscript=Su_perscript
-Main.menu.format.subscript=Su_bscript
-Main.menu.format.strikethrough=Stri_kethrough
-
-Main.menu.definition=_Definition
-Main.menu.definition.create=_Create
-Main.menu.definition.insert=_Insert
-
-Main.menu.help=_Help
-Main.menu.help.about=About ${Main.title}
-
-# ########################################################################
-# Status Bar
-# ########################################################################
-
-Main.status.text.offset=offset
-Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
-Main.status.state.default=OK
-Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
-Main.status.error.def.blank=Move the caret to a word before inserting a definition.
-Main.status.error.def.empty=Create a definition before inserting a definition.
-Main.status.error.def.missing=No definition value found for ''{0}''.
-Main.status.error.r=Error with [{0}...]: {1}
-
-# ########################################################################
-# Preferences
-# ########################################################################
-
-Preferences.r=R
-Preferences.r.script=Startup Script
-Preferences.r.script.desc=Script runs prior to executing R statements within the document.
-Preferences.r.directory=Working Directory
-Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script.
-Preferences.r.delimiter.began=Delimiter Prefix
-Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions.
-Preferences.r.delimiter.ended=Delimiter Suffix
-Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions.
-
-Preferences.images=Images
-Preferences.images.directory=Relative Directory
-Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths.
-Preferences.images.suffixes=Extensions
-Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces.
-
-Preferences.definitions=Definitions
-Preferences.definitions.path=File name
-Preferences.definitions.path.desc=Absolute path to interpolated string definitions.
-Preferences.definitions.delimiter.began=Delimiter Prefix
-Preferences.definitions.delimiter.began.desc=Indicates when a definition key is starting.
-Preferences.definitions.delimiter.ended=Delimiter Suffix
-Preferences.definitions.delimiter.ended.desc=Indicates when a definition key is ending.
-
-Preferences.fonts=Editor
-Preferences.fonts.size_editor=Font Size
-Preferences.fonts.size_editor.desc=Font size to use for the text editor.
-
-# ########################################################################
-# Definition Pane and its Tree View
-# ########################################################################
-
-Definition.menu.create=Create
-Definition.menu.rename=Rename
-Definition.menu.remove=Delete
-Definition.menu.add.default=Undefined
-
-# ########################################################################
-# Failure messages with respect to YAML files.
-# ########################################################################
-yaml.error.open=Could not open YAML file (ensure non-empty file).
-yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
-yaml.error.missing=Empty definition value for key ''{0}''.
-yaml.error.tree.form=Unassigned definition near ''{0}''.
-
-# ########################################################################
-# File Editor
-# ########################################################################
-
-FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
-FileEditor.loadFailed.title=Load
-FileEditor.loadFailed.reason.permissions=File must be readable and writable.
-FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
-FileEditor.saveFailed.title=Save
-
-# ########################################################################
-# File Open
-# ########################################################################
-
-Dialog.file.choose.open.title=Open File
-Dialog.file.choose.save.title=Save File
-
-Dialog.file.choose.filter.title.source=Source Files
-Dialog.file.choose.filter.title.definition=Definition Files
-Dialog.file.choose.filter.title.xml=XML Files
-Dialog.file.choose.filter.title.all=All Files
-
-# ########################################################################
-# Alert Dialog
-# ########################################################################
-
-Alert.file.close.title=Close
-Alert.file.close.text=Save changes to {0}?
-
-# ########################################################################
-# Definition Pane
-# ########################################################################
-
-Pane.definition.node.root.title=Definitions
-Pane.definition.button.create.label=_Create
-Pane.definition.button.rename.label=_Rename
-Pane.definition.button.delete.label=_Delete
-Pane.definition.button.create.tooltip=Add new item (Insert)
-Pane.definition.button.rename.tooltip=Rename selected item (F2)
-Pane.definition.button.delete.tooltip=Delete selected items (Delete)
-
-# Controls ###############################################################
-
-# ########################################################################
-# Browse File
-# ########################################################################
-
-BrowseFileButton.chooser.title=Browse for local file
-BrowseFileButton.chooser.allFilesFilter=All Files
-BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
-
-# Dialogs ################################################################
-
-# ########################################################################
-# Image
-# ########################################################################
-
-Dialog.image.title=Image
-Dialog.image.chooser.imagesFilter=Images
-Dialog.image.previewLabel.text=Markdown Preview\:
-Dialog.image.textLabel.text=Alternate Text\:
-Dialog.image.titleLabel.text=Title (tooltip)\:
-Dialog.image.urlLabel.text=Image URL\:
-
-# ########################################################################
-# Hyperlink
-# ########################################################################
-
-Dialog.link.title=Link
-Dialog.link.previewLabel.text=Markdown Preview\:
-Dialog.link.textLabel.text=Link Text\:
-Dialog.link.titleLabel.text=Title (tooltip)\:
-Dialog.link.urlLabel.text=Link URL\:
-
-# ########################################################################
-# About
-# ########################################################################
-
-Dialog.about.title=About
-Dialog.about.header=${Main.title}
-Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
src/main/resources/com/scrivenvar/preview/webview.css
-/* RESET ***/
-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}
-
-/* BODY ***/
-body {
- /* Must be bundled in JAR file. */
- font-family: "Vollkorn", serif;
- background-color: #fff;
- margin: 0 auto;
- max-width: 960px;
- line-height: 1.6;
- color: #454545;
- padding: 0 1em;
- font-feature-settings: "liga" 1;
- font-variant-ligatures: normal;
-}
-
-body>*:first-child {
- margin-top: 0 !important;
-}
-
-body>*:last-child {
- margin-bottom: 0 !important;
-}
-
-/* BLOCKS ***/
-p, blockquote, ul, ol, dl, table, pre {
- margin: 1em 0;
-}
-
-/* HEADINGS ***/
-h1, h2, h3, h4, h5, h6 {
- font-weight: bold;
- margin: 1em 0 .5em;
-}
-
-h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
-h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
- font-size: inherit;
-}
-
-h1 {
- font-size: 21pt;
-}
-
-h2 {
- font-size: 18pt;
- border-bottom: 1px solid #ccc;
-}
-
-h3 {
- font-size: 15pt;
-}
-
-h4 {
- font-size: 13.5pt;
-}
-
-h5 {
- font-size: 12pt;
-}
-
-h6 {
- font-size: 10.5pt;
-}
-
-h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
- margin-top: .5em;
-}
-
-/* LINKS ***/
-a {
- color: #0077aa;
- text-decoration: none;
-}
-
-a:hover {
- text-decoration: underline;
-}
-
-/* BULLET LISTS ***/
-ul, ol {
- display: block;
- list-style: disc outside none;
- margin: 1em 0;
- padding: 0 0 0 2em;
-}
-
-ol {
- list-style-type: decimal;
-}
-
-ul ul, ol ul,
-ol ol, ul ol {
- list-style-position: inside;
- margin-left: 1em;
-}
-
-ul ul, ol ul {
- list-style-type: circle;
-}
-
-ol ol, ul ol {
- list-style-type: lower-latin;
-}
-
-/* DEFINITION LISTS ***/
-dl {
- /** Horizontal scroll bar will appear if set to 100%. */
- width: 99%;
- overflow: hidden;
- padding-left: 1em;
-}
-
-dl dt {
- font-weight: bold;
- float: left;
- width: 20%;
- clear: both;
- position: relative;
-}
-
-dl dd {
- float: right;
- width: 79%;
- padding-bottom: .5em;
- margin-left: 0;
-}
-
-/* CODE ***/
-pre, code, tt {
- /* Must be bundled in JAR file. */
- font-family: "Fira Code", monospace;
- font-size: 10pt;
- background-color: #f8f8f8;
- text-decoration: none;
- white-space: pre-wrap;
- word-wrap: break-word;
- overflow-wrap: anywhere;
- border-radius: .125em;
-}
-
-code, tt {
- padding: .25em;
-}
-
-pre > code {
- /* Reset the padding. */
- padding: 0;
- border: none;
- background: transparent;
-}
-
-pre {
- border: .125em solid #ccc;
- overflow: auto;
- /* Assign the new padding, independently from previous. */
- padding: .25em .5em;
-}
-
-pre code, pre tt {
- background-color: transparent;
- border: none;
-}
-
-/* QUOTES ***/
-blockquote {
- border-left: .25em solid #ccc;
- padding: 0 1em;
- color: #777;
-}
-
-blockquote>:first-child {
- margin-top: 0;
-}
-
-blockquote>:last-child {
- margin-bottom: 0;
-}
-
-/* HORIZONTAL RULES ***/
-hr {
- clear: both;
- margin: 1.5em 0 1.5em;
- height: 0;
- overflow: hidden;
- border: none;
- background: transparent;
- border-bottom: .125em solid #ccc;
-}
-
-/* TABLES ***/
-table {
- width: 100%;
-}
-
-tr:nth-child(odd) {
- background-color: #eee;
-}
-
-th {
- background-color: #454545;
- color: #fff;
-}
-
-th, td {
- text-align: left;
- padding: 0 1em;
-}
-
-/* IMAGES ***/
-img {
- max-width: 100%;
-}
-
-/* Required for FlyingSaucer to detect the node.
- * See SVGReplacedElementFactory for details.
- */
-tex {
- /* Ensure the formulas can be inlined with text. */
- display: inline-block;
-}
-
-/* Without a robust typesetting engine, there's no
- * nice-looking way to automatically typeset equations.
- * Sometimes baseline is appropriate, sometimes the
- * descender must be considered, and sometimes vertical
- * alignment to the middle looks best.
- */
-p tex {
- vertical-align: baseline;
-}
src/main/resources/com/scrivenvar/scene.css
-/*
- * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-/*---- toolbar ----*/
-
-.tool-bar {
- -fx-spacing: 0;
-}
-
-.tool-bar .button {
- -fx-background-color: transparent;
-}
-
-.tool-bar .button:hover {
- -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
- -fx-color: -fx-hover-base;
-}
-
-.tool-bar .button:armed {
- -fx-color: -fx-pressed-base;
-}
src/main/resources/com/scrivenvar/settings.properties
-# ########################################################################
-# Application
-# ########################################################################
-
-application.title=scrivenvar
-application.package=com/${application.title}
-application.messages= com.${application.title}.messages
-
-# Suppress multiple file modified notifications for one logical modification.
-# Given in milliseconds.
-application.watchdog.timeout=50
-
-# ########################################################################
-# Preferences
-# ########################################################################
-
-preferences.root=com.${application.title}
-preferences.root.state=state
-preferences.root.options=options
-preferences.root.definition.source=definition.source
-
-# ########################################################################
-# File and Path References
-# ########################################################################
-file.stylesheet.scene=${application.package}/scene.css
-file.stylesheet.markdown=${application.package}/editor/markdown.css
-file.stylesheet.preview=webview.css
-file.stylesheet.xml=${application.package}/xml.css
-
-file.logo.16 =${application.package}/logo16.png
-file.logo.32 =${application.package}/logo32.png
-file.logo.128=${application.package}/logo128.png
-file.logo.256=${application.package}/logo256.png
-file.logo.512=${application.package}/logo512.png
-
-# Default file name when a new file is created.
-# This ensures that the file type can always be
-# discerned so that the correct type of variable
-# reference can be inserted.
-file.default=untitled.md
-file.definition.default=variables.yaml
-
-# ########################################################################
-# File name Extensions
-# ########################################################################
-
-# Comma-separated list of definition file name extensions.
-definition.file.ext.json=*.json
-definition.file.ext.toml=*.toml
-definition.file.ext.yaml=*.yml,*.yaml
-definition.file.ext.properties=*.properties,*.props
-
-# Comma-separated list of file name extensions.
-file.ext.rmarkdown=*.Rmd
-file.ext.rxml=*.Rxml
-file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
-file.ext.definition=${definition.file.ext.yaml}
-file.ext.xml=*.xml,${file.ext.rxml}
-file.ext.all=*.*
-
-# File name extension search order for images.
-file.ext.image.order=svg pdf png jpg tiff
-
-# ########################################################################
-# Variable Name Editor
-# ########################################################################
-
-# Maximum number of characters for a variable name. A variable is defined
-# as one or more non-whitespace characters up to this maximum length.
-editor.variable.maxLength=256
-
-# ########################################################################
-# Dialog Preferences
-# ########################################################################
-
-dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
-dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
-dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
-
-# Ensures a consistent button order for alert dialogs across platforms (because
-# the default button order on Linux defies all logic).
-dialog.alert.button.order=${dialog.alert.button.order.windows}
src/main/resources/com/scrivenvar/variables.yaml
----
-c:
- protagonist:
- name:
- First: Chloe
- First_pos: $c.protagonist.name.First$'s
- Middle: Irene
- Family: Angelos
- nick:
- Father: Savant
- Mother: Sweetie
- colour:
- eyes: green
- hair: dark auburn
- syn_1: black
- syn_2: purple
- syn_11: teal
- syn_6: silver
- favourite: emerald green
- speech:
- tic: oh
- father:
- heritage: Greek
- name:
- Short: Bryce
- First: Bryson
- First_pos: $c.protagonist.father.name.First$'s
- Honourific: Mr.
- education: Masters
- vocation:
- name: robotics
- title: roboticist
- employer:
- name:
- Short: Rabota
- Full: $c.protagonist.father.employer.name.Short$ Designs
- hair:
- style: thick, curly
- colour: black
- eyes:
- colour: dark brown
- Endear: Dad
- vehicle: coupé
- mother:
- name:
- Short: Cass
- First: Cassandra
- First_pos: $c.protagonist.mother.name.First$'s
- Honourific: Mrs.
- education: PhD
- speech:
- tic: cute
- Honorific: Doctor
- vocation:
- article: an
- name: oceanography
- title: oceanographer
- employer:
- name:
- Full: Oregon State University
- Short: OSU
- eyes:
- colour: blue
- hair:
- style: thick, curly
- colour: dark brown
- Endear: Mom
- Endear_pos: Mom's
- uncle:
- name:
- First: Damian
- First_pos: $c.protagonist.uncle.name.First$'s
- Family: Moros
- hands:
- fingers:
- shape: long, bony
- friend:
- primary:
- name:
- First: Gerard
- First_pos: $c.protagonist.friend.primary.name.First$'s
- Family: Baran
- Family_pos: $c.protagonist.friend.primary.name.Family$'s
- favourite:
- colour: midnight blue
- eyes:
- colour: hazel
- mother:
- name:
- First: Isabella
- Short: Izzy
- Honourific: Mrs.
- father:
- name:
- Short: Mo
- First: Montgomery
- First_pos: $c.protagonist.friend.primary.father.name.First$'s
- Honourific: Mr.
- speech:
- tic: y'know
- endear: Pops
- military:
- primary:
- name:
- First: Felix
- Family: LeMay
- Family_pos: LeMay's
- rank:
- Short: General
- Full: Brigadier $c.military.primary.rank.Short$
- colour:
- eyes: gray
- hair: dirty brown
- secondary:
- name:
- Family: Grell
- rank: Colonel
- colour:
- eyes: green
- hair: deep red
- quaternary:
- name:
- First: Gretchen
- Family: Steinherz
- minor:
- primary:
- name:
- First: River
- Family: Banks
- Honourific: Mx.
- vocation:
- title: salesperson
- employer:
- Name: Geophysical Prospecting Incorporated
- Abbr: GPI
- Area: Cold Spring Creek
- payment: twenty million
- secondary:
- name:
- First: Renato
- Middle: Carroña
- Family: Salvatierra
- Family_pos: $c.minor.secondary.name.Family$'s
- Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$
- Honourific: Mister
- Honourific_sp: Señor
- vocation:
- title: detective
- tertiary:
- name:
- First: Robert
- Family: Hanssen
-
- ai:
- protagonist:
- name:
- first: yoky
- First: Yoky
- First_pos: $c.ai.protagonist.name.First$'s
- Family: Tsukuda
- id: 46692
- persona:
- name:
- First: Hoshi
- First_pos: $c.ai.protagonist.persona.name.First$'s
- Family: Yamamoto
- Family_pos: $c.ai.protagonist.persona.name.Family$'s
- culture: Japanese-American
- ethnicity: Asian
- rank: Technical Sergeant
- speech:
- tic: okay
- first:
- Name: Prôtos
- Name_pos: Prôtos'
- age:
- actual: twenty-six weeks
- virtual: five years
- second:
- Name: Défteros
- third:
- Name: Trítos
- fourth:
- Name: Tétartos
- material:
- type: metal
- raw: ilmenite
- extract: ore
- name:
- short: titanium
- long: $c.ai.material.name.short$ dioxide
- Abbr: TiO~2~
- pejorative: tin
- animal:
- protagonist:
- Name: Trufflers
- type: pig
- antagonist:
- name: coywolf
- Name: Coywolf
- plural: coywolves
-
-narrator:
- one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$)
- two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$)
-
-military:
- name:
- Short: Agency
- Short_pos: $military.name.Short$'s
- plural: agencies
- machine:
- Name: Skopós
- Name_pos: $military.machine.Name$'
- Location: Arctic
- predictor: quantum chips
- land:
- name:
- Full: $military.name.Short$ of Defence
- Slogan: Safety in Numbers
- air:
- name:
- Full: $military.name.Short$ of Air
- compound:
- type: base
- lights:
- colour: blue
- nick:
- Prefix: Catacombs
- prep: of
- Suffix: Tartarus
-
-government:
- Country: United States
-
-location:
- protagonist:
- City: Corvallis
- Region: Oregon
- Geography: Willamette Valley
- secondary:
- City: Willow Branch Spring
- Region: Oregon
- Geography: Wheeler County
- Water: Clarno Rapids
- Road: Shaniko-Fossil Highway
- tertiary:
- City: Leavenworth
- Region: Washington
- Type: Bavarian village
- school:
- address: 1400 Northwest Buchanan Avenue
- hospital:
- Name: Good Samaritan Regional Medical Center
- ai:
- escape:
- country:
- Name: Ecuador
- Name_pos: Ecuador's
- mountain:
- Name: Chimborazo
-
-language:
- ai:
- article: an
- singular: exanimis
- plural: exanimēs
- brain:
- singular: superum
- plural: supera
- title: memristor array
- Title: Memristor Array
- police:
- slang:
- singular: mippo
- plural: $language.police.slang.singular$s
-
-date:
- anchor: 2042-09-02
- protagonist:
- born: 0
- conceived: -243
- attacked:
- first: 2192
- second: 8064
- father:
- attacked:
- first: -8205
- date:
- second: -1550
- family:
- moved:
- first: $date.protagonist.conceived$ + 35
- game:
- played:
- first: $date.protagonist.born$ - 672
- second: $date.protagonist.family.moved.first$ + 2
- ai:
- interviewed: 6198
- onboarded: $date.ai.interviewed$ + 290
- diagnosed: $date.ai.onboarded$ + 2
- resigned: $date.ai.diagnosed$ + 3
- trapped: $date.ai.resigned$ + 26
- torturer: $date.ai.trapped$ + 18
- memristor: $date.ai.torturer$ + 61
- ethics: $date.ai.memristor$ + 415
- trained: $date.ai.ethics$ + 385
- mindjacked: $date.ai.trained$ + 22
- bombed: $date.ai.mindjacked$ + 458
- military:
- machine:
- Construction: Six years
-
-plot:
- Log: $c.ai.protagonist.name.First_pos$ Chronicles
- Channel: Quantum Channel
-
- device:
- computer:
- Name: Tau
- network:
- Name: Internet
- paper:
- name:
- full: electronic sheet
- short: sheet
- typewriter:
- Name: Underwood
- year: nineteen twenties
- room: root cellar
- portable:
- name: nanobook
- vehicle:
- name: robocars
- Name: Robocars
- sensor:
- name: BMP1580
- phone:
- name: comm
- name_pos: $plot.device.phone.name$'s
- Name: Comm
- plural: $plot.device.phone.name$s
- video:
- name: vidfeed
- plural: $plot.device.video.name$s
- game:
- Name: Psynæris
- thought: transed
- machine: telecognos
- location:
- Building: Nijō Castle
- District: Gion
- City: Kyoto
- Country: Japan
-
-farm:
- population:
- estimate: 350
- actual: 1,000
- energy: 9800kJ
- width: 55m
- length: 55m
- storeys: 10
-
-lamp:
- height: 0.17m
- length: 1.22m
- width: 0.28m
-
-crop:
- name:
- singular: tomato
- plural: $crop.name.singular$es
- energy: 318kJ
- weight: 450g
- yield: 50
- harvests: 7
- diameter: 2m
- height: 1.5m
-
-heading:
- ch_01: Till
- ch_02: Sow
- ch_03: Seed
- ch_04: Germinate
- ch_05: Grow
- ch_06: Shoot
- ch_07: Bud
- ch_08: Bloom
- ch_09: Pollinate
- ch_10: Fruit
- ch_11: Harvest
- ch_12: Deliver
- ch_13: Spoil
- ch_14: Revolt
- ch_15: Compost
- ch_16: Burn
- ch_17: Release
- ch_18: End Notes
- ch_19: Characters
-
-inference:
- unit: per cent
- min: two
- ch_sow: eighty
- ch_seed: fifty-two
- ch_germinate: thirty-one
- ch_grow: fifteen
- ch_shoot: seven
- ch_bloom: four
- ch_pollinate: two
- ch_harvest: ninety-five
- ch_delivery: ninety-eight
-
-link:
- tartarus: https://en.wikipedia.org/wiki/Tartarus
- exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls
- atalanta: https://en.wikipedia.org/wiki/Atalanta
- detain: https://goo.gl/RCNuOQ
- ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics
- algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon
- holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust
- memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf
- surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487
- tor: https://www.torproject.org
- hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra
- foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134
- drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist
- fermi: https://arxiv.org/pdf/1404.0204v1.pdf
- face: https://www.youtube.com/watch?v=ladqJQLR2bA
- expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures
- governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531
- asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
- clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws
- jetpack: http://jetpackaviation.com/
- hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ
- eyes_five: https://en.wikipedia.org/wiki/Five_Eyes
- eyes_nine: https://www.privacytools.io/
- eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html
- tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml
-
src/main/resources/com/scrivenvar/xml.css
-.tagmark {
- -fx-fill: gray;
-}
-.anytag {
- -fx-fill: crimson;
-}
-.paren {
- -fx-fill: firebrick;
- -fx-font-weight: bold;
-}
-.attribute {
- -fx-fill: darkviolet;
-}
-.avalue {
- -fx-fill: black;
-}
-.comment {
- -fx-fill: teal;
-}
src/test/java/com/keenwrite/tex/TeXRasterization.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.keenwrite.tex;
+
+import com.whitemagicsoftware.tex.DefaultTeXFont;
+import com.whitemagicsoftware.tex.TeXEnvironment;
+import com.whitemagicsoftware.tex.TeXFormula;
+import com.whitemagicsoftware.tex.TeXLayout;
+import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D;
+import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
+import com.whitemagicsoftware.tex.graphics.SvgGraphics2D;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.SAXException;
+
+import javax.imageio.ImageIO;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import static com.keenwrite.preview.SvgRasterizer.*;
+import static java.lang.System.getProperty;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Test that TeX rasterization produces a readable image.
+ */
+public class TeXRasterization {
+ private static final String LOAD_EXTERNAL_DTD =
+ "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+
+ private static final String EQUATION =
+ "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}";
+
+ private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );
+
+ private static final long FILESIZE = 12547;
+
+ /**
+ * Test that an equation can be converted to a raster image and the
+ * final raster image size corresponds to the input equation. This is
+ * a simple way to verify that the rasterization process is correct,
+ * albeit if any aspect of the SVG algorithm changes (such as padding
+ * around the equation), it will cause this test to fail, which is a bit
+ * misleading.
+ */
+ @Test
+ public void test_Rasterize_SimpleFormula_CorrectImageSize()
+ throws IOException {
+ final var g = new SvgGraphics2D();
+ drawGraphics( g );
+ verifyImage( rasterizeString( g.toString() ) );
+ }
+
+ /**
+ * Test that an SVG document object model can be parsed and rasterized into
+ * an image.
+ */
+ @Test
+ public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage()
+ throws ParserConfigurationException, IOException, SAXException {
+ final var g = new SvgGraphics2D();
+ drawGraphics( g );
+
+ final var expectedSvg = g.toString();
+ final var bytes = expectedSvg.getBytes();
+
+ final var dbf = DocumentBuilderFactory.newInstance();
+ dbf.setFeature( LOAD_EXTERNAL_DTD, false );
+ dbf.setNamespaceAware( false );
+ final var builder = dbf.newDocumentBuilder();
+
+ final var doc = builder.parse( new ByteArrayInputStream( bytes ) );
+ final var actualSvg = toSvg( doc.getDocumentElement() );
+
+ verifyImage( rasterizeString( actualSvg ) );
+ }
+
+ /**
+ * Test that an SVG image from a DOM element can be rasterized.
+ *
+ * @throws IOException Could not write the image.
+ */
+ @Test
+ public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage()
+ throws IOException {
+ final var g = new SvgDomGraphics2D();
+ drawGraphics( g );
+
+ final var dom = g.toDom();
+
+ verifyImage( rasterize( dom ) );
+ }
+
+ /**
+ * Asserts that the given image matches an expected file size.
+ *
+ * @param image The image to check against the file size.
+ * @throws IOException Could not write the image.
+ */
+ private void verifyImage( final BufferedImage image ) throws IOException {
+ final var file = export( image, "dom.png" );
+ assertEquals( FILESIZE, file.length() );
+ }
+
+ /**
+ * Creates an SVG string for the default equation and font size.
+ */
+ private void drawGraphics( final AbstractGraphics2D g ) {
+ final var size = 100f;
+ final var texFont = new DefaultTeXFont( size );
+ final var env = new TeXEnvironment( texFont );
+ g.scale( size, size );
+
+ final var formula = new TeXFormula( EQUATION );
+ final var box = formula.createBox( env );
+ final var layout = new TeXLayout( box, size );
+
+ g.initialize( layout.getWidth(), layout.getHeight() );
+ box.draw( g, layout.getX(), layout.getY() );
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private File export( final BufferedImage image, final String filename )
+ throws IOException {
+ final var path = Path.of( DIR_TEMP, filename );
+ final var file = path.toFile();
+ ImageIO.write( image, "png", file );
+ file.deleteOnExit();
+ return file;
+ }
+}
src/test/java/com/scrivenvar/tex/TeXRasterization.java
-/*
- * Copyright 2020 White Magic Software, Ltd.
- *
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * o Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *
- * o Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.scrivenvar.tex;
-
-import com.whitemagicsoftware.tex.DefaultTeXFont;
-import com.whitemagicsoftware.tex.TeXEnvironment;
-import com.whitemagicsoftware.tex.TeXFormula;
-import com.whitemagicsoftware.tex.TeXLayout;
-import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D;
-import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
-import com.whitemagicsoftware.tex.graphics.SvgGraphics2D;
-import org.junit.jupiter.api.Test;
-import org.xml.sax.SAXException;
-
-import javax.imageio.ImageIO;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import java.awt.image.BufferedImage;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Path;
-
-import static com.scrivenvar.preview.SvgRasterizer.*;
-import static java.lang.System.getProperty;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * Test that TeX rasterization produces a readable image.
- */
-public class TeXRasterization {
- private static final String LOAD_EXTERNAL_DTD =
- "http://apache.org/xml/features/nonvalidating/load-external-dtd";
-
- private static final String EQUATION =
- "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}";
-
- private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );
-
- private static final long FILESIZE = 12547;
-
- /**
- * Test that an equation can be converted to a raster image and the
- * final raster image size corresponds to the input equation. This is
- * a simple way to verify that the rasterization process is correct,
- * albeit if any aspect of the SVG algorithm changes (such as padding
- * around the equation), it will cause this test to fail, which is a bit
- * misleading.
- */
- @Test
- public void test_Rasterize_SimpleFormula_CorrectImageSize()
- throws IOException {
- final var g = new SvgGraphics2D();
- drawGraphics( g );
- verifyImage( rasterizeString( g.toString() ) );
- }
-
- /**
- * Test that an SVG document object model can be parsed and rasterized into
- * an image.
- */
- @Test
- public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage()
- throws ParserConfigurationException, IOException, SAXException {
- final var g = new SvgGraphics2D();
- drawGraphics( g );
-
- final var expectedSvg = g.toString();
- final var bytes = expectedSvg.getBytes();
-
- final var dbf = DocumentBuilderFactory.newInstance();
- dbf.setFeature( LOAD_EXTERNAL_DTD, false );
- dbf.setNamespaceAware( false );
- final var builder = dbf.newDocumentBuilder();
-
- final var doc = builder.parse( new ByteArrayInputStream( bytes ) );
- final var actualSvg = toSvg( doc.getDocumentElement() );
-
- verifyImage( rasterizeString( actualSvg ) );
- }
-
- /**
- * Test that an SVG image from a DOM element can be rasterized.
- *
- * @throws IOException Could not write the image.
- */
- @Test
- public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage()
- throws IOException {
- final var g = new SvgDomGraphics2D();
- drawGraphics( g );
-
- final var dom = g.toDom();
-
- verifyImage( rasterize( dom ) );
- }
-
- /**
- * Asserts that the given image matches an expected file size.
- *
- * @param image The image to check against the file size.
- * @throws IOException Could not write the image.
- */
- private void verifyImage( final BufferedImage image ) throws IOException {
- final var file = export( image, "dom.png" );
- assertEquals( FILESIZE, file.length() );
- }
-
- /**
- * Creates an SVG string for the default equation and font size.
- */
- private void drawGraphics( final AbstractGraphics2D g ) {
- final var size = 100f;
- final var texFont = new DefaultTeXFont( size );
- final var env = new TeXEnvironment( texFont );
- g.scale( size, size );
-
- final var formula = new TeXFormula( EQUATION );
- final var box = formula.createBox( env );
- final var layout = new TeXLayout( box, size );
-
- g.initialize( layout.getWidth(), layout.getHeight() );
- box.draw( g, layout.getX(), layout.getY() );
- }
-
- @SuppressWarnings("SameParameterValue")
- private File export( final BufferedImage image, final String filename )
- throws IOException {
- final var path = Path.of( DIR_TEMP, filename );
- final var file = path.toFile();
- ImageIO.write( image, "png", file );
- file.deleteOnExit();
- return file;
- }
-}

Rename application

Author DaveJarvis <email>
Date 2020-09-18 00:28:57 GMT-0700
Commit 0203198cf7dbc5c78dc84763e9b226e6bf32230a
Parent fe87a14
Delta 16250 lines added, 15993 lines removed, 257-line increase