| dist | ||
| -scrivenvar.bin | ||
| -scrivenvar.exe | ||
| +*.bin | ||
| +*.exe | ||
| build | ||
| .gradle |
| 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 |
| -#  Scrivenvar | ||
| +#  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 | ||
| ``` | ||
| --- | ||
| application: | ||
| - title: "Scrivenvar" | ||
| + title: "KeenWrite" | ||
| +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() | ||
| 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 | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ) { | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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( '\\', '/' ) ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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() ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| +} | ||
| +/* | ||
| + * 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() ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| +} | ||
| +/* | ||
| + * 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() ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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() ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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( "", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | ||
| + .otherwise( Bindings.format( "", 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 | ||
| +} | ||
| +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 ) | ||
| + } ) | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 | ||
| +} | ||
| +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 ) | ||
| + } ) | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + }; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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() ); | ||
| + } | ||
| +} |
| +/* | ||
| + * 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 ); | ||
| + } | ||
| + } | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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'> </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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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() { | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ) ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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>"; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +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 ) { | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ) { | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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() { | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ] ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ) ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| +} | ||
| +/* | ||
| + * 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 { | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| +} | ||
| +/* | ||
| + * 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 ) { | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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(); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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 ); | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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() | ||
| + ); | ||
| + } | ||
| +} | ||
| +/* | ||
| + * 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) ); | ||
| + } | ||
| + } | ||
| +} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ) { | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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( '\\', '/' ) ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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() ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| -} | ||
| -/* | ||
| - * 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() ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| -} | ||
| -/* | ||
| - * 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() ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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() ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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( "", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | ||
| - .otherwise( Bindings.format( "", 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 | ||
| -} | ||
| -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 ) | ||
| - } ) | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 | ||
| -} | ||
| -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 ) | ||
| - } ) | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - }; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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() ); | ||
| - } | ||
| -} |
| -/* | ||
| - * 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 ); | ||
| - } | ||
| - } | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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'> </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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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() { | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ) ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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>"; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -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 ) { | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ) { | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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() { | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ] ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ) ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| -} | ||
| -/* | ||
| - * 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 { | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| -} | ||
| -/* | ||
| - * 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 ) { | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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(); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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 ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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() | ||
| - ); | ||
| - } | ||
| -} | ||
| -/* | ||
| - * 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) ); | ||
| - } | ||
| - } | ||
| -} | ||
| - | ||
| +com.keenwrite.service.impl.DefaultOptions |
| - | ||
| +com.keenwrite.service.impl.DefaultSettings |
| - | ||
| +com.keenwrite.service.impl.DefaultSnitch |
| - | ||
| +com.keenwrite.service.events.impl.DefaultNotifier |
| -com.scrivenvar.service.impl.DefaultOptions | ||
| + |
| -com.scrivenvar.service.impl.DefaultSettings | ||
| + |
| -com.scrivenvar.service.impl.DefaultSnitch | ||
| + |
| -com.scrivenvar.service.events.impl.DefaultNotifier | ||
| + |
| +app.properties | ||
| +#!/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 | ||
| + | ||
| +/* | ||
| + * 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; | ||
| +} | ||
| - | ||
| +<?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> |
| +<?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> | ||
| +<?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> | ||
| +# ######################################################################## | ||
| +# 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 | ||
| +/* 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; | ||
| +} | ||
| +/* | ||
| + * 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; | ||
| +} | ||
| +# ######################################################################## | ||
| +# 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} | ||
| +--- | ||
| +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 | ||
| + | ||
| +.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; | ||
| +} |
| -app.properties | ||
| -#!/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 | ||
| - | ||
| -/* | ||
| - * 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; | ||
| -} | ||
| -<?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> | ||
| -# ######################################################################## | ||
| -# 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 | ||
| -/* 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; | ||
| -} | ||
| -/* | ||
| - * 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; | ||
| -} | ||
| -# ######################################################################## | ||
| -# 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} | ||
| ---- | ||
| -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 | ||
| - | ||
| -.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; | ||
| -} |
| +/* | ||
| + * 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; | ||
| + } | ||
| +} | ||
| -/* | ||
| - * 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; | ||
| - } | ||
| -} | ||
| 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 |