| 1 | 1 | dist |
| 2 | scrivenvar.bin | |
| 3 | scrivenvar.exe | |
| 2 | *.bin | |
| 3 | *.exe | |
| 4 | 4 | build |
| 5 | 5 | .gradle |
| 22 | 22 | After the application is compiled, run it as follows: |
| 23 | 23 | |
| 24 | java -jar build/libs/scrivenvar.jar | |
| 24 | java -jar build/libs/keenwrite.jar | |
| 25 | 25 | |
| 26 | 26 | On Windows: |
| 27 | 27 | |
| 28 | java -jar build\libs\scrivenvar.jar | |
| 28 | java -jar build\libs\keenwrite.jar | |
| 29 | 29 | |
| 30 | 30 | # Installers |
| 1 | 1 | # License |
| 2 | 2 | |
| 3 | Copyright 2020 White Magic Software, Ltd. | |
| 4 | ||
| 3 | 5 | Copyright 2015 Karl Tauber |
| 4 | All rights reserved. | |
| 5 | 6 | |
| 6 | Copyright 2020 White Magic Software, Ltd. | |
| 7 | 7 | All rights reserved. |
| 8 | 8 |
| 1 | #  Scrivenvar | |
| 1 | #  | |
| 2 | 2 | |
| 3 | 3 | A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values. |
| 4 | 4 | |
| 5 | 5 | ## Download |
| 6 | 6 | |
| 7 | 7 | Download one of the following editions: |
| 8 | 8 | |
| 9 | * [Windows](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.exe) | |
| 10 | * [Linux](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.bin) | |
| 11 | * [Java Archive](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.jar) | |
| 9 | * [Windows](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.exe) | |
| 10 | * [Linux](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.bin) | |
| 11 | * [Java Archive](https://gitreleases.dev/gh/DaveJarvis/keenwrite/latest/keenwrite.jar) | |
| 12 | 12 | |
| 13 | 13 | ## Run |
| ... | ||
| 21 | 21 | When upgrading to a new version, delete the following directory; |
| 22 | 22 | |
| 23 | C:\Users\%USERNAME%\AppData\Local\warp\packages\scrivenvar.exe | |
| 23 | C:\Users\%USERNAME%\AppData\Local\warp\packages\keenwrite.exe | |
| 24 | 24 | |
| 25 | 25 | ### Linux |
| 26 | 26 | |
| 27 | On Linux, run `chmod +x scrivenvar.bin` then `./scrivenvar.bin`. | |
| 27 | On Linux, run `chmod +x keenwrite.bin` then `./keenwrite.bin`. | |
| 28 | 28 | |
| 29 | 29 | ### Other |
| 30 | 30 | |
| 31 | On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/) that includes JavaFX module support, then run: | |
| 31 | On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/pages/downloads/?version=java-14#mn) that includes JavaFX module support, then run: | |
| 32 | 32 | |
| 33 | 33 | ``` bash |
| 34 | java -jar scrivenvar.jar | |
| 34 | java -jar keenwrite.jar | |
| 35 | 35 | ``` |
| 36 | 36 | |
| ... | ||
| 51 | 51 | using the application. |
| 52 | 52 | |
| 53 | ## Screenshots | |
| 53 | ## Screenshot | |
| 54 | 54 | |
| 55 | 55 |  |
| 56 | ||
| 57 |  | |
| 58 | 56 | |
| 59 | 57 | ## License |
| 1 | --- | |
| 2 | application: | |
| 3 | title: "Scrivenvar" | |
| 4 | 1 |
| 123 | 123 | } |
| 124 | 124 | |
| 125 | def resourceDir = sourceSets.main.resources.srcDirs[0] | |
| 126 | ||
| 127 | def config = new Properties() | |
| 128 | file("${resourceDir}/bootstrap.properties").withInputStream { | |
| 129 | config.load(it) | |
| 130 | } | |
| 131 | ||
| 125 | 132 | application { |
| 126 | applicationName = 'scrivenvar' | |
| 133 | applicationName = config["application.title"].toLowerCase() | |
| 127 | 134 | mainClassName = "com.${applicationName}.Main" |
| 128 | 135 | |
| ... | ||
| 138 | 145 | def launcherClassName = "com.${applicationName}.Launcher" |
| 139 | 146 | |
| 140 | def propertiesFile = new File("src/main/resources/com/${applicationName}/app.properties") | |
| 147 | def propertiesFile = new File("${resourceDir}/com/${applicationName}/app.properties") | |
| 141 | 148 | propertiesFile.write("application.version=${version}") |
| 142 | 149 | |
| 1 | #  | |
| 2 | ||
| 1 | 3 | # Real-time equation rendering |
| 2 | 4 | |
| 3 | With interpolated variables and R calculations: | |
| 5 | Interpolated variables within R calculations, formatted as an equation: | |
| 4 | 6 | |
| 5 | $\sqrt{`r#x( v$formula$sqrt$value)`} = `r# round(sqrt(x( v$formula$sqrt$value )),5)`$ | |
| 7 | $\sqrt{`r#x( v$formula$sqrt$value)`} = \pm `r# round(sqrt(x( v$formula$sqrt$value )),5)`$ | |
| 6 | 8 | |
| 7 | 9 | # Maxwell's equations |
| 2 | 2 | formula: |
| 3 | 3 | sqrt: |
| 4 | value: "603" | |
| 4 | value: "42" | |
| 5 | 5 |
| 155 | 155 | |
| 156 | 156 | set SCRIPT_DIR=%~dp0 |
| 157 | "%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" -jar "%SCRIPT_DIR%\\scrivenvar.jar" %* | |
| 157 | "%SCRIPT_DIR%\\${ARG_JAVA_DIR}\\bin\\java" -jar "%SCRIPT_DIR%\\${APP_NAME}.jar" %* | |
| 158 | 158 | __EOT |
| 159 | 159 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.keenwrite.service.Settings; | |
| 31 | import com.keenwrite.util.ProtocolScheme; | |
| 32 | ||
| 33 | import java.nio.file.Path; | |
| 34 | ||
| 35 | import static com.keenwrite.Constants.GLOB_PREFIX_FILE; | |
| 36 | import static com.keenwrite.Constants.SETTINGS; | |
| 37 | import static com.keenwrite.FileType.UNKNOWN; | |
| 38 | import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate; | |
| 39 | import static java.lang.String.format; | |
| 40 | ||
| 41 | /** | |
| 42 | * Provides common behaviours for factories that instantiate classes based on | |
| 43 | * file type. | |
| 44 | */ | |
| 45 | public class AbstractFileFactory { | |
| 46 | ||
| 47 | private static final String MSG_UNKNOWN_FILE_TYPE = | |
| 48 | "Unknown type '%s' for file '%s'."; | |
| 49 | ||
| 50 | /** | |
| 51 | * Determines the file type from the path extension. This should only be | |
| 52 | * called when it is known that the file type won't be a definition file | |
| 53 | * (e.g., YAML or other definition source), but rather an editable file | |
| 54 | * (e.g., Markdown, XML, etc.). | |
| 55 | * | |
| 56 | * @param path The path with a file name extension. | |
| 57 | * @return The FileType for the given path. | |
| 58 | */ | |
| 59 | public FileType lookup( final Path path ) { | |
| 60 | return lookup( path, GLOB_PREFIX_FILE ); | |
| 61 | } | |
| 62 | ||
| 63 | /** | |
| 64 | * Creates a file type that corresponds to the given path. | |
| 65 | * | |
| 66 | * @param path Reference to a variable definition file. | |
| 67 | * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE. | |
| 68 | * @return The file type that corresponds to the given path. | |
| 69 | */ | |
| 70 | protected FileType lookup( final Path path, final String prefix ) { | |
| 71 | assert path != null; | |
| 72 | assert prefix != null; | |
| 73 | ||
| 74 | final var settings = getSettings(); | |
| 75 | final var keys = settings.getKeys( prefix ); | |
| 76 | ||
| 77 | var found = false; | |
| 78 | var fileType = UNKNOWN; | |
| 79 | ||
| 80 | while( keys.hasNext() && !found ) { | |
| 81 | final var key = keys.next(); | |
| 82 | final var patterns = settings.getStringSettingList( key ); | |
| 83 | final var predicate = createFileTypePredicate( patterns ); | |
| 84 | ||
| 85 | if( found = predicate.test( path.toFile() ) ) { | |
| 86 | // Remove the EXTENSIONS_PREFIX to get the filename extension mapped | |
| 87 | // to a standard name (as defined in the settings.properties file). | |
| 88 | final String suffix = key.replace( prefix + ".", "" ); | |
| 89 | fileType = FileType.from( suffix ); | |
| 90 | } | |
| 91 | } | |
| 92 | ||
| 93 | return fileType; | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Throws IllegalArgumentException because the given path could not be | |
| 98 | * recognized. This exists because | |
| 99 | * | |
| 100 | * @param type The detected path type (protocol, file extension, etc.). | |
| 101 | * @param path The path to a source of definitions. | |
| 102 | */ | |
| 103 | protected void unknownFileType( | |
| 104 | final ProtocolScheme type, final String path ) { | |
| 105 | final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path ); | |
| 106 | throw new IllegalArgumentException( msg ); | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * Return the singleton Settings instance. | |
| 111 | * | |
| 112 | * @return A non-null instance. | |
| 113 | */ | |
| 114 | private Settings getSettings() { | |
| 115 | return SETTINGS; | |
| 116 | } | |
| 117 | } | |
| 1 | 118 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import java.io.IOException; | |
| 31 | import java.util.Properties; | |
| 32 | ||
| 33 | /** | |
| 34 | * Responsible for loading the bootstrap.properties file, which is | |
| 35 | * tactically located outside of the standard resource reverse domain name | |
| 36 | * namespace to avoid hard-coding the application name in many places. | |
| 37 | * Instead, the application name is located in the bootstrap file, which is | |
| 38 | * then used to look-up the remaining settings. | |
| 39 | * <p> | |
| 40 | * See {@link Constants#PATH_PROPERTIES_SETTINGS} for details. | |
| 41 | * </p> | |
| 42 | */ | |
| 43 | public class Bootstrap { | |
| 44 | private static final Properties BOOTSTRAP = new Properties(); | |
| 45 | ||
| 46 | static { | |
| 47 | try( final var stream = | |
| 48 | Constants.class.getResourceAsStream( "/bootstrap.properties" ) ) { | |
| 49 | BOOTSTRAP.load( stream ); | |
| 50 | } catch( final IOException ignored ) { | |
| 51 | // Bootstrap properties cannot be found, throw in the towel. | |
| 52 | } | |
| 53 | } | |
| 54 | ||
| 55 | public static final String APP_TITLE = | |
| 56 | BOOTSTRAP.getProperty( "application.title" ); | |
| 57 | public static final String APP_TITLE_LOWERCASE = APP_TITLE.toLowerCase(); | |
| 58 | } | |
| 1 | 59 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.keenwrite.service.Settings; | |
| 31 | import javafx.scene.image.Image; | |
| 32 | ||
| 33 | import java.nio.file.Path; | |
| 34 | import java.nio.file.Paths; | |
| 35 | ||
| 36 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | |
| 37 | import static java.lang.String.format; | |
| 38 | ||
| 39 | /** | |
| 40 | * Defines application-wide default values. | |
| 41 | */ | |
| 42 | public class Constants { | |
| 43 | ||
| 44 | /** | |
| 45 | * Used by the default settings to load the {@link Settings} service. This | |
| 46 | * must come before any attempt is made to create a {@link Settings} object. | |
| 47 | * The reference to {@link Bootstrap#APP_TITLE_LOWERCASE} should cause the | |
| 48 | * JVM to load {@link Bootstrap} prior to proceeding. Loading that class | |
| 49 | * beforehand will read the bootstrap properties file to determine the | |
| 50 | * application name, which is then used to locate the settings properties. | |
| 51 | */ | |
| 52 | public static final String PATH_PROPERTIES_SETTINGS = | |
| 53 | format( "/com/%s/settings.properties", APP_TITLE_LOWERCASE ); | |
| 54 | ||
| 55 | /** | |
| 56 | * The {@link Settings} uses {@link #PATH_PROPERTIES_SETTINGS}. | |
| 57 | */ | |
| 58 | public static final Settings SETTINGS = Services.load( Settings.class ); | |
| 59 | ||
| 60 | public static final String DEFINITION_NAME = "variables.yaml"; | |
| 61 | ||
| 62 | public static final String APP_BUNDLE_NAME = get( "application.messages" ); | |
| 63 | ||
| 64 | // Prevent double events when updating files on Linux (save and timestamp). | |
| 65 | public static final int APP_WATCHDOG_TIMEOUT = get( | |
| 66 | "application.watchdog.timeout", 200 ); | |
| 67 | ||
| 68 | public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" ); | |
| 69 | public static final String STYLESHEET_MARKDOWN = get( | |
| 70 | "file.stylesheet.markdown" ); | |
| 71 | public static final String STYLESHEET_PREVIEW = get( | |
| 72 | "file.stylesheet.preview" ); | |
| 73 | ||
| 74 | public static final String FILE_LOGO_16 = get( "file.logo.16" ); | |
| 75 | public static final String FILE_LOGO_32 = get( "file.logo.32" ); | |
| 76 | public static final String FILE_LOGO_128 = get( "file.logo.128" ); | |
| 77 | public static final String FILE_LOGO_256 = get( "file.logo.256" ); | |
| 78 | public static final String FILE_LOGO_512 = get( "file.logo.512" ); | |
| 79 | ||
| 80 | public static final Image ICON_DIALOG = new Image( FILE_LOGO_32 ); | |
| 81 | ||
| 82 | public static final String PREFS_ROOT = get( "preferences.root" ); | |
| 83 | public static final String PREFS_STATE = get( "preferences.root.state" ); | |
| 84 | ||
| 85 | /** | |
| 86 | * Refer to filename extension settings in the configuration file. Do not | |
| 87 | * terminate these prefixes with a period. | |
| 88 | */ | |
| 89 | public static final String GLOB_PREFIX_FILE = "file.ext"; | |
| 90 | public static final String GLOB_PREFIX_DEFINITION = | |
| 91 | "definition." + GLOB_PREFIX_FILE; | |
| 92 | ||
| 93 | /** | |
| 94 | * Three parameters: line number, column number, and offset. | |
| 95 | */ | |
| 96 | public static final String STATUS_BAR_LINE = "Main.status.line"; | |
| 97 | ||
| 98 | public static final String STATUS_BAR_OK = "Main.status.state.default"; | |
| 99 | ||
| 100 | /** | |
| 101 | * Used to show an error while parsing, usually syntactical. | |
| 102 | */ | |
| 103 | public static final String STATUS_PARSE_ERROR = "Main.status.error.parse"; | |
| 104 | public static final String STATUS_DEFINITION_BLANK = | |
| 105 | "Main.status.error.def.blank"; | |
| 106 | public static final String STATUS_DEFINITION_EMPTY = | |
| 107 | "Main.status.error.def.empty"; | |
| 108 | ||
| 109 | /** | |
| 110 | * One parameter: the word under the cursor that could not be found. | |
| 111 | */ | |
| 112 | public static final String STATUS_DEFINITION_MISSING = | |
| 113 | "Main.status.error.def.missing"; | |
| 114 | ||
| 115 | /** | |
| 116 | * Used when creating flat maps relating to resolved variables. | |
| 117 | */ | |
| 118 | public static final int DEFAULT_MAP_SIZE = 64; | |
| 119 | ||
| 120 | /** | |
| 121 | * Default image extension order to use when scanning. | |
| 122 | */ | |
| 123 | public static final String PERSIST_IMAGES_DEFAULT = | |
| 124 | get( "file.ext.image.order" ); | |
| 125 | ||
| 126 | /** | |
| 127 | * Default working directory to use for R startup script. | |
| 128 | */ | |
| 129 | public static final String USER_DIRECTORY = System.getProperty( "user.dir" ); | |
| 130 | ||
| 131 | /** | |
| 132 | * Default path to use for an untitled (pathless) file. | |
| 133 | */ | |
| 134 | public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY ); | |
| 135 | ||
| 136 | /** | |
| 137 | * Default starting delimiter for definition variables. | |
| 138 | */ | |
| 139 | public static final String DEF_DELIM_BEGAN_DEFAULT = "${"; | |
| 140 | ||
| 141 | /** | |
| 142 | * Default ending delimiter for definition variables. | |
| 143 | */ | |
| 144 | public static final String DEF_DELIM_ENDED_DEFAULT = "}"; | |
| 145 | ||
| 146 | /** | |
| 147 | * Default starting delimiter when inserting R variables. | |
| 148 | */ | |
| 149 | public static final String R_DELIM_BEGAN_DEFAULT = "x( "; | |
| 150 | ||
| 151 | /** | |
| 152 | * Default ending delimiter when inserting R variables. | |
| 153 | */ | |
| 154 | public static final String R_DELIM_ENDED_DEFAULT = " )"; | |
| 155 | ||
| 156 | /** | |
| 157 | * Resource directory where different language lexicons are located. | |
| 158 | */ | |
| 159 | public static final String LEXICONS_DIRECTORY = "lexicons"; | |
| 160 | ||
| 161 | /** | |
| 162 | * Used as the prefix for uniquely identifying HTML block elements, which | |
| 163 | * helps coordinate scrolling the preview pane to where the user is typing. | |
| 164 | */ | |
| 165 | public static final String PARAGRAPH_ID_PREFIX = "p-"; | |
| 166 | ||
| 167 | /** | |
| 168 | * Absolute location of true type font files within the Java archive file. | |
| 169 | */ | |
| 170 | public static final String FONT_DIRECTORY = "/fonts"; | |
| 171 | ||
| 172 | /** | |
| 173 | * Default text editor font size, in points. | |
| 174 | */ | |
| 175 | public static final float FONT_SIZE_EDITOR = 12f; | |
| 176 | ||
| 177 | /** | |
| 178 | * Prevent instantiation. | |
| 179 | */ | |
| 180 | private Constants() { | |
| 181 | } | |
| 182 | ||
| 183 | private static String get( final String key ) { | |
| 184 | return SETTINGS.getSetting( key, "" ); | |
| 185 | } | |
| 186 | ||
| 187 | @SuppressWarnings("SameParameterValue") | |
| 188 | private static int get( final String key, final int defaultValue ) { | |
| 189 | return SETTINGS.getSetting( key, defaultValue ); | |
| 190 | } | |
| 191 | } | |
| 1 | 192 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * Redistribution and use in source and binary forms, with or without | |
| 5 | * modification, are permitted provided that the following conditions are met: | |
| 6 | * | |
| 7 | * o Redistributions of source code must retain the above copyright | |
| 8 | * notice, this list of conditions and the following disclaimer. | |
| 9 | * | |
| 10 | * o Redistributions in binary form must reproduce the above copyright | |
| 11 | * notice, this list of conditions and the following disclaimer in the | |
| 12 | * documentation and/or other materials provided with the distribution. | |
| 13 | * | |
| 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 15 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 17 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 18 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 19 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 20 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 21 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 22 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 25 | */ | |
| 26 | package com.keenwrite; | |
| 27 | ||
| 28 | import com.keenwrite.editors.EditorPane; | |
| 29 | import com.keenwrite.editors.markdown.MarkdownEditorPane; | |
| 30 | import com.keenwrite.service.events.Notification; | |
| 31 | import com.keenwrite.service.events.Notifier; | |
| 32 | import javafx.beans.binding.Bindings; | |
| 33 | import javafx.beans.property.BooleanProperty; | |
| 34 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 35 | import javafx.beans.property.ReadOnlyBooleanWrapper; | |
| 36 | import javafx.beans.property.SimpleBooleanProperty; | |
| 37 | import javafx.beans.value.ChangeListener; | |
| 38 | import javafx.event.Event; | |
| 39 | import javafx.event.EventHandler; | |
| 40 | import javafx.event.EventType; | |
| 41 | import javafx.scene.Scene; | |
| 42 | import javafx.scene.control.Tab; | |
| 43 | import javafx.scene.control.Tooltip; | |
| 44 | import javafx.scene.text.Text; | |
| 45 | import javafx.stage.Window; | |
| 46 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 47 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 48 | import org.fxmisc.undo.UndoManager; | |
| 49 | import org.jetbrains.annotations.NotNull; | |
| 50 | import org.mozilla.universalchardet.UniversalDetector; | |
| 51 | ||
| 52 | import java.io.File; | |
| 53 | import java.nio.charset.Charset; | |
| 54 | import java.nio.file.Files; | |
| 55 | import java.nio.file.Path; | |
| 56 | ||
| 57 | import static com.keenwrite.Messages.get; | |
| 58 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 59 | import static com.keenwrite.StatusBarNotifier.getNotifier; | |
| 60 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 61 | import static java.util.Locale.ENGLISH; | |
| 62 | import static javafx.application.Platform.runLater; | |
| 63 | ||
| 64 | /** | |
| 65 | * Editor for a single file. | |
| 66 | */ | |
| 67 | public final class FileEditorTab extends Tab { | |
| 68 | ||
| 69 | private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane(); | |
| 70 | ||
| 71 | private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper(); | |
| 72 | private final BooleanProperty canUndo = new SimpleBooleanProperty(); | |
| 73 | private final BooleanProperty canRedo = new SimpleBooleanProperty(); | |
| 74 | ||
| 75 | /** | |
| 76 | * Character encoding used by the file (or default encoding if none found). | |
| 77 | */ | |
| 78 | private Charset mEncoding = UTF_8; | |
| 79 | ||
| 80 | /** | |
| 81 | * File to load into the editor. | |
| 82 | */ | |
| 83 | private Path mPath; | |
| 84 | ||
| 85 | public FileEditorTab( final Path path ) { | |
| 86 | setPath( path ); | |
| 87 | ||
| 88 | mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() ); | |
| 89 | ||
| 90 | setOnSelectionChanged( e -> { | |
| 91 | if( isSelected() ) { | |
| 92 | runLater( this::activated ); | |
| 93 | requestFocus(); | |
| 94 | } | |
| 95 | } ); | |
| 96 | } | |
| 97 | ||
| 98 | private void updateTab() { | |
| 99 | setText( getTabTitle() ); | |
| 100 | setGraphic( getModifiedMark() ); | |
| 101 | setTooltip( getTabTooltip() ); | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Returns the base filename (without the directory names). | |
| 106 | * | |
| 107 | * @return The untitled text if the path hasn't been set. | |
| 108 | */ | |
| 109 | private String getTabTitle() { | |
| 110 | return getPath().getFileName().toString(); | |
| 111 | } | |
| 112 | ||
| 113 | /** | |
| 114 | * Returns the full filename represented by the path. | |
| 115 | * | |
| 116 | * @return The untitled text if the path hasn't been set. | |
| 117 | */ | |
| 118 | private Tooltip getTabTooltip() { | |
| 119 | final Path filePath = getPath(); | |
| 120 | return new Tooltip( filePath == null ? "" : filePath.toString() ); | |
| 121 | } | |
| 122 | ||
| 123 | /** | |
| 124 | * Returns a marker to indicate whether the file has been modified. | |
| 125 | * | |
| 126 | * @return "*" when the file has changed; otherwise null. | |
| 127 | */ | |
| 128 | private Text getModifiedMark() { | |
| 129 | return isModified() ? new Text( "*" ) : null; | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * Called when the user switches tab. | |
| 134 | */ | |
| 135 | private void activated() { | |
| 136 | // Tab is closed or no longer active. | |
| 137 | if( getTabPane() == null || !isSelected() ) { | |
| 138 | return; | |
| 139 | } | |
| 140 | ||
| 141 | // If the tab is devoid of content, load it. | |
| 142 | if( getContent() == null ) { | |
| 143 | readFile(); | |
| 144 | initLayout(); | |
| 145 | initUndoManager(); | |
| 146 | } | |
| 147 | } | |
| 148 | ||
| 149 | private void initLayout() { | |
| 150 | setContent( getScrollPane() ); | |
| 151 | } | |
| 152 | ||
| 153 | /** | |
| 154 | * Tracks undo requests, but can only be called <em>after</em> load. | |
| 155 | */ | |
| 156 | private void initUndoManager() { | |
| 157 | final UndoManager<?> undoManager = getUndoManager(); | |
| 158 | undoManager.forgetHistory(); | |
| 159 | ||
| 160 | // Bind the editor undo manager to the properties. | |
| 161 | mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) ); | |
| 162 | canUndo.bind( undoManager.undoAvailableProperty() ); | |
| 163 | canRedo.bind( undoManager.redoAvailableProperty() ); | |
| 164 | } | |
| 165 | ||
| 166 | private void requestFocus() { | |
| 167 | getEditorPane().requestFocus(); | |
| 168 | } | |
| 169 | ||
| 170 | /** | |
| 171 | * Searches from the caret position forward for the given string. | |
| 172 | * | |
| 173 | * @param needle The text string to match. | |
| 174 | */ | |
| 175 | public void searchNext( final String needle ) { | |
| 176 | final String haystack = getEditorText(); | |
| 177 | int index = haystack.indexOf( needle, getCaretPosition() ); | |
| 178 | ||
| 179 | // Wrap around. | |
| 180 | if( index == -1 ) { | |
| 181 | index = haystack.indexOf( needle ); | |
| 182 | } | |
| 183 | ||
| 184 | if( index >= 0 ) { | |
| 185 | setCaretPosition( index ); | |
| 186 | getEditor().selectRange( index, index + needle.length() ); | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | /** | |
| 191 | * Gets a reference to the scroll pane that houses the editor. | |
| 192 | * | |
| 193 | * @return The editor's scroll pane, containing a vertical scrollbar. | |
| 194 | */ | |
| 195 | public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { | |
| 196 | return getEditorPane().getScrollPane(); | |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * Returns the index into the text where the caret blinks happily away. | |
| 201 | * | |
| 202 | * @return A number from 0 to the editor's document text length. | |
| 203 | */ | |
| 204 | public int getCaretPosition() { | |
| 205 | return getEditor().getCaretPosition(); | |
| 206 | } | |
| 207 | ||
| 208 | /** | |
| 209 | * Moves the caret to a given offset. | |
| 210 | * | |
| 211 | * @param offset The new caret offset. | |
| 212 | */ | |
| 213 | private void setCaretPosition( final int offset ) { | |
| 214 | getEditor().moveTo( offset ); | |
| 215 | getEditor().requestFollowCaret(); | |
| 216 | } | |
| 217 | ||
| 218 | /** | |
| 219 | * Returns the text area associated with this tab. | |
| 220 | * | |
| 221 | * @return A text editor. | |
| 222 | */ | |
| 223 | private StyleClassedTextArea getEditor() { | |
| 224 | return getEditorPane().getEditor(); | |
| 225 | } | |
| 226 | ||
| 227 | /** | |
| 228 | * Returns true if the given path exactly matches this tab's path. | |
| 229 | * | |
| 230 | * @param check The path to compare against. | |
| 231 | * @return true The paths are the same. | |
| 232 | */ | |
| 233 | public boolean isPath( final Path check ) { | |
| 234 | final Path filePath = getPath(); | |
| 235 | ||
| 236 | return filePath != null && filePath.equals( check ); | |
| 237 | } | |
| 238 | ||
| 239 | /** | |
| 240 | * Reads the entire file contents from the path associated with this tab. | |
| 241 | */ | |
| 242 | private void readFile() { | |
| 243 | final Path path = getPath(); | |
| 244 | final File file = path.toFile(); | |
| 245 | ||
| 246 | try { | |
| 247 | if( file.exists() ) { | |
| 248 | if( file.canWrite() && file.canRead() ) { | |
| 249 | final EditorPane pane = getEditorPane(); | |
| 250 | pane.setText( asString( Files.readAllBytes( path ) ) ); | |
| 251 | pane.scrollToTop(); | |
| 252 | } | |
| 253 | else { | |
| 254 | final String msg = get( "FileEditor.loadFailed.reason.permissions" ); | |
| 255 | alert( "FileEditor.loadFailed.message", file.toString(), msg ); | |
| 256 | } | |
| 257 | } | |
| 258 | } catch( final Exception ex ) { | |
| 259 | alert( ex ); | |
| 260 | } | |
| 261 | } | |
| 262 | ||
| 263 | /** | |
| 264 | * Saves the entire file contents from the path associated with this tab. | |
| 265 | * | |
| 266 | * @return true The file has been saved. | |
| 267 | */ | |
| 268 | public boolean save() { | |
| 269 | try { | |
| 270 | final EditorPane editor = getEditorPane(); | |
| 271 | Files.write( getPath(), asBytes( editor.getText() ) ); | |
| 272 | editor.getUndoManager().mark(); | |
| 273 | return true; | |
| 274 | } catch( final Exception ex ) { | |
| 275 | return popupAlert( | |
| 276 | "FileEditor.saveFailed.title", | |
| 277 | "FileEditor.saveFailed.message", | |
| 278 | ex | |
| 279 | ); | |
| 280 | } | |
| 281 | } | |
| 282 | ||
| 283 | /** | |
| 284 | * Creates an alert dialog and waits for it to close. | |
| 285 | * | |
| 286 | * @param titleKey Resource bundle key for the alert dialog title. | |
| 287 | * @param messageKey Resource bundle key for the alert dialog message. | |
| 288 | * @param e The unexpected happening. | |
| 289 | * @return false | |
| 290 | */ | |
| 291 | @SuppressWarnings("SameParameterValue") | |
| 292 | private boolean popupAlert( | |
| 293 | final String titleKey, final String messageKey, final Exception e ) { | |
| 294 | final Notifier service = getNotifier(); | |
| 295 | final Path filePath = getPath(); | |
| 296 | ||
| 297 | final Notification message = service.createNotification( | |
| 298 | get( titleKey ), | |
| 299 | get( messageKey ), | |
| 300 | filePath == null ? "" : filePath, | |
| 301 | e.getMessage() | |
| 302 | ); | |
| 303 | ||
| 304 | try { | |
| 305 | service.createError( getWindow(), message ).showAndWait(); | |
| 306 | } catch( final Exception ex ) { | |
| 307 | alert( ex ); | |
| 308 | } | |
| 309 | ||
| 310 | return false; | |
| 311 | } | |
| 312 | ||
| 313 | private Window getWindow() { | |
| 314 | final Scene scene = getEditorPane().getScene(); | |
| 315 | ||
| 316 | if( scene == null ) { | |
| 317 | throw new UnsupportedOperationException( "No scene window available" ); | |
| 318 | } | |
| 319 | ||
| 320 | return scene.getWindow(); | |
| 321 | } | |
| 322 | ||
| 323 | /** | |
| 324 | * Returns a best guess at the file encoding. If the encoding could not be | |
| 325 | * detected, this will return the default charset for the JVM. | |
| 326 | * | |
| 327 | * @param bytes The bytes to perform character encoding detection. | |
| 328 | * @return The character encoding. | |
| 329 | */ | |
| 330 | private Charset detectEncoding( final byte[] bytes ) { | |
| 331 | final var detector = new UniversalDetector( null ); | |
| 332 | detector.handleData( bytes, 0, bytes.length ); | |
| 333 | detector.dataEnd(); | |
| 334 | ||
| 335 | final String charset = detector.getDetectedCharset(); | |
| 336 | ||
| 337 | return charset == null | |
| 338 | ? Charset.defaultCharset() | |
| 339 | : Charset.forName( charset.toUpperCase( ENGLISH ) ); | |
| 340 | } | |
| 341 | ||
| 342 | /** | |
| 343 | * Converts the given string to an array of bytes using the encoding that was | |
| 344 | * originally detected (if any) and associated with this file. | |
| 345 | * | |
| 346 | * @param text The text to convert into the original file encoding. | |
| 347 | * @return A series of bytes ready for writing to a file. | |
| 348 | */ | |
| 349 | private byte[] asBytes( final String text ) { | |
| 350 | return text.getBytes( getEncoding() ); | |
| 351 | } | |
| 352 | ||
| 353 | /** | |
| 354 | * Converts the given bytes into a Java String. This will call setEncoding | |
| 355 | * with the encoding detected by the CharsetDetector. | |
| 356 | * | |
| 357 | * @param text The text of unknown character encoding. | |
| 358 | * @return The text, in its auto-detected encoding, as a String. | |
| 359 | */ | |
| 360 | private String asString( final byte[] text ) { | |
| 361 | setEncoding( detectEncoding( text ) ); | |
| 362 | return new String( text, getEncoding() ); | |
| 363 | } | |
| 364 | ||
| 365 | /** | |
| 366 | * Returns the path to the file being edited in this tab. | |
| 367 | * | |
| 368 | * @return A non-null instance. | |
| 369 | */ | |
| 370 | public Path getPath() { | |
| 371 | return mPath; | |
| 372 | } | |
| 373 | ||
| 374 | /** | |
| 375 | * Sets the path to a file for editing and then updates the tab with the | |
| 376 | * file contents. | |
| 377 | * | |
| 378 | * @param path A non-null instance. | |
| 379 | */ | |
| 380 | public void setPath( final Path path ) { | |
| 381 | assert path != null; | |
| 382 | mPath = path; | |
| 383 | ||
| 384 | updateTab(); | |
| 385 | } | |
| 386 | ||
| 387 | public boolean isModified() { | |
| 388 | return mModified.get(); | |
| 389 | } | |
| 390 | ||
| 391 | ReadOnlyBooleanProperty modifiedProperty() { | |
| 392 | return mModified.getReadOnlyProperty(); | |
| 393 | } | |
| 394 | ||
| 395 | BooleanProperty canUndoProperty() { | |
| 396 | return this.canUndo; | |
| 397 | } | |
| 398 | ||
| 399 | BooleanProperty canRedoProperty() { | |
| 400 | return this.canRedo; | |
| 401 | } | |
| 402 | ||
| 403 | private UndoManager<?> getUndoManager() { | |
| 404 | return getEditorPane().getUndoManager(); | |
| 405 | } | |
| 406 | ||
| 407 | /** | |
| 408 | * Forwards to the editor pane's listeners for text change events. | |
| 409 | * | |
| 410 | * @param listener The listener to notify when the text changes. | |
| 411 | */ | |
| 412 | public void addTextChangeListener( final ChangeListener<String> listener ) { | |
| 413 | getEditorPane().addTextChangeListener( listener ); | |
| 414 | } | |
| 415 | ||
| 416 | /** | |
| 417 | * Forwards to the editor pane's listeners for caret change events. | |
| 418 | * | |
| 419 | * @param listener Notified when the caret position changes. | |
| 420 | */ | |
| 421 | public void addCaretPositionListener( | |
| 422 | final ChangeListener<? super Integer> listener ) { | |
| 423 | getEditorPane().addCaretPositionListener( listener ); | |
| 424 | } | |
| 425 | ||
| 426 | /** | |
| 427 | * Forwards to the editor pane's listeners for paragraph index change events. | |
| 428 | * | |
| 429 | * @param listener Notified when the caret's paragraph index changes. | |
| 430 | */ | |
| 431 | public void addCaretParagraphListener( | |
| 432 | final ChangeListener<? super Integer> listener ) { | |
| 433 | getEditorPane().addCaretParagraphListener( listener ); | |
| 434 | } | |
| 435 | ||
| 436 | public <T extends Event> void addEventFilter( | |
| 437 | final EventType<T> eventType, | |
| 438 | final EventHandler<? super T> eventFilter ) { | |
| 439 | getEditor().addEventFilter( eventType, eventFilter ); | |
| 440 | } | |
| 441 | ||
| 442 | /** | |
| 443 | * Forwards the request to the editor pane. | |
| 444 | * | |
| 445 | * @return The text to process. | |
| 446 | */ | |
| 447 | public String getEditorText() { | |
| 448 | return getEditorPane().getText(); | |
| 449 | } | |
| 450 | ||
| 451 | /** | |
| 452 | * Returns the editor pane, or creates one if it doesn't yet exist. | |
| 453 | * | |
| 454 | * @return The editor pane, never null. | |
| 455 | */ | |
| 456 | @NotNull | |
| 457 | public MarkdownEditorPane getEditorPane() { | |
| 458 | return mEditorPane; | |
| 459 | } | |
| 460 | ||
| 461 | /** | |
| 462 | * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been | |
| 463 | * determined. | |
| 464 | * | |
| 465 | * @return The file encoding or UTF-8 if unknown. | |
| 466 | */ | |
| 467 | private Charset getEncoding() { | |
| 468 | return mEncoding; | |
| 469 | } | |
| 470 | ||
| 471 | private void setEncoding( final Charset encoding ) { | |
| 472 | assert encoding != null; | |
| 473 | mEncoding = encoding; | |
| 474 | } | |
| 475 | ||
| 476 | /** | |
| 477 | * Returns the tab title, without any modified indicators. | |
| 478 | * | |
| 479 | * @return The tab title. | |
| 480 | */ | |
| 481 | @Override | |
| 482 | public String toString() { | |
| 483 | return getTabTitle(); | |
| 484 | } | |
| 485 | } | |
| 1 | 486 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.keenwrite.service.Options; | |
| 31 | import com.keenwrite.service.Settings; | |
| 32 | import com.keenwrite.service.events.Notification; | |
| 33 | import com.keenwrite.service.events.Notifier; | |
| 34 | import com.keenwrite.util.Utils; | |
| 35 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 36 | import javafx.beans.property.ReadOnlyBooleanWrapper; | |
| 37 | import javafx.beans.property.ReadOnlyObjectProperty; | |
| 38 | import javafx.beans.property.ReadOnlyObjectWrapper; | |
| 39 | import javafx.beans.value.ChangeListener; | |
| 40 | import javafx.collections.ListChangeListener; | |
| 41 | import javafx.collections.ObservableList; | |
| 42 | import javafx.event.Event; | |
| 43 | import javafx.scene.control.Alert; | |
| 44 | import javafx.scene.control.ButtonType; | |
| 45 | import javafx.scene.control.Tab; | |
| 46 | import javafx.scene.control.TabPane; | |
| 47 | import javafx.stage.FileChooser; | |
| 48 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 49 | import javafx.stage.Window; | |
| 50 | ||
| 51 | import java.io.File; | |
| 52 | import java.nio.file.Path; | |
| 53 | import java.util.ArrayList; | |
| 54 | import java.util.List; | |
| 55 | import java.util.Optional; | |
| 56 | import java.util.concurrent.atomic.AtomicReference; | |
| 57 | import java.util.prefs.Preferences; | |
| 58 | import java.util.stream.Collectors; | |
| 59 | ||
| 60 | import static com.keenwrite.Constants.GLOB_PREFIX_FILE; | |
| 61 | import static com.keenwrite.Constants.SETTINGS; | |
| 62 | import static com.keenwrite.FileType.*; | |
| 63 | import static com.keenwrite.Messages.get; | |
| 64 | import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate; | |
| 65 | import static com.keenwrite.service.events.Notifier.YES; | |
| 66 | ||
| 67 | /** | |
| 68 | * Tab pane for file editors. | |
| 69 | */ | |
| 70 | public final class FileEditorTabPane extends TabPane { | |
| 71 | ||
| 72 | private static final String FILTER_EXTENSION_TITLES = | |
| 73 | "Dialog.file.choose.filter"; | |
| 74 | ||
| 75 | private static final Options sOptions = Services.load( Options.class ); | |
| 76 | private static final Notifier sNotifier = Services.load( Notifier.class ); | |
| 77 | ||
| 78 | private final ReadOnlyObjectWrapper<Path> mOpenDefinition = | |
| 79 | new ReadOnlyObjectWrapper<>(); | |
| 80 | private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor = | |
| 81 | new ReadOnlyObjectWrapper<>(); | |
| 82 | private final ReadOnlyBooleanWrapper mAnyFileEditorModified = | |
| 83 | new ReadOnlyBooleanWrapper(); | |
| 84 | private final ChangeListener<Integer> mCaretPositionListener; | |
| 85 | private final ChangeListener<Integer> mCaretParagraphListener; | |
| 86 | ||
| 87 | /** | |
| 88 | * Constructs a new file editor tab pane. | |
| 89 | * | |
| 90 | * @param caretPositionListener Listens for changes to caret position so | |
| 91 | * that the status bar can update. | |
| 92 | * @param caretParagraphListener Listens for changes to the caret's paragraph | |
| 93 | * so that scrolling may occur. | |
| 94 | */ | |
| 95 | public FileEditorTabPane( | |
| 96 | final ChangeListener<Integer> caretPositionListener, | |
| 97 | final ChangeListener<Integer> caretParagraphListener ) { | |
| 98 | final ObservableList<Tab> tabs = getTabs(); | |
| 99 | ||
| 100 | setFocusTraversable( false ); | |
| 101 | setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | |
| 102 | ||
| 103 | addTabSelectionListener( | |
| 104 | ( tabPane, oldTab, newTab ) -> { | |
| 105 | if( newTab != null ) { | |
| 106 | mActiveFileEditor.set( (FileEditorTab) newTab ); | |
| 107 | } | |
| 108 | } | |
| 109 | ); | |
| 110 | ||
| 111 | final ChangeListener<Boolean> modifiedListener = | |
| 112 | ( observable, oldValue, newValue ) -> { | |
| 113 | for( final Tab tab : tabs ) { | |
| 114 | if( ((FileEditorTab) tab).isModified() ) { | |
| 115 | mAnyFileEditorModified.set( true ); | |
| 116 | break; | |
| 117 | } | |
| 118 | } | |
| 119 | }; | |
| 120 | ||
| 121 | tabs.addListener( | |
| 122 | (ListChangeListener<Tab>) change -> { | |
| 123 | while( change.next() ) { | |
| 124 | if( change.wasAdded() ) { | |
| 125 | change.getAddedSubList().forEach( | |
| 126 | ( tab ) -> { | |
| 127 | final var fet = (FileEditorTab) tab; | |
| 128 | fet.modifiedProperty().addListener( modifiedListener ); | |
| 129 | } ); | |
| 130 | } | |
| 131 | else if( change.wasRemoved() ) { | |
| 132 | change.getRemoved().forEach( | |
| 133 | ( tab ) -> { | |
| 134 | final var fet = (FileEditorTab) tab; | |
| 135 | fet.modifiedProperty().removeListener( modifiedListener ); | |
| 136 | } | |
| 137 | ); | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | // Changes in the tabs may also change anyFileEditorModified property | |
| 142 | // (e.g. closed modified file) | |
| 143 | modifiedListener.changed( null, null, null ); | |
| 144 | } | |
| 145 | ); | |
| 146 | ||
| 147 | mCaretPositionListener = caretPositionListener; | |
| 148 | mCaretParagraphListener = caretParagraphListener; | |
| 149 | } | |
| 150 | ||
| 151 | /** | |
| 152 | * Allows observers to be notified when the current file editor tab changes. | |
| 153 | * | |
| 154 | * @param listener The listener to notify of tab change events. | |
| 155 | */ | |
| 156 | public void addTabSelectionListener( final ChangeListener<Tab> listener ) { | |
| 157 | // Observe the tab so that when a new tab is opened or selected, | |
| 158 | // a notification is kicked off. | |
| 159 | getSelectionModel().selectedItemProperty().addListener( listener ); | |
| 160 | } | |
| 161 | ||
| 162 | /** | |
| 163 | * Returns the tab that has keyboard focus. | |
| 164 | * | |
| 165 | * @return A non-null instance. | |
| 166 | */ | |
| 167 | public FileEditorTab getActiveFileEditor() { | |
| 168 | return mActiveFileEditor.get(); | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Returns the property corresponding to the tab that has focus. | |
| 173 | * | |
| 174 | * @return A non-null instance. | |
| 175 | */ | |
| 176 | public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | |
| 177 | return mActiveFileEditor.getReadOnlyProperty(); | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Property that can answer whether the text has been modified. | |
| 182 | * | |
| 183 | * @return A non-null instance, true meaning the content has not been saved. | |
| 184 | */ | |
| 185 | ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | |
| 186 | return mAnyFileEditorModified.getReadOnlyProperty(); | |
| 187 | } | |
| 188 | ||
| 189 | /** | |
| 190 | * Creates a new editor instance from the given path. | |
| 191 | * | |
| 192 | * @param path The file to open. | |
| 193 | * @return A non-null instance. | |
| 194 | */ | |
| 195 | private FileEditorTab createFileEditor( final Path path ) { | |
| 196 | assert path != null; | |
| 197 | ||
| 198 | final FileEditorTab tab = new FileEditorTab( path ); | |
| 199 | ||
| 200 | tab.setOnCloseRequest( e -> { | |
| 201 | if( !canCloseEditor( tab ) ) { | |
| 202 | e.consume(); | |
| 203 | } | |
| 204 | else if( isActiveFileEditor( tab ) ) { | |
| 205 | // Prevent prompting the user to save when there are no file editor | |
| 206 | // tabs open. | |
| 207 | mActiveFileEditor.set( null ); | |
| 208 | } | |
| 209 | } ); | |
| 210 | ||
| 211 | tab.addCaretPositionListener( mCaretPositionListener ); | |
| 212 | tab.addCaretParagraphListener( mCaretParagraphListener ); | |
| 213 | ||
| 214 | return tab; | |
| 215 | } | |
| 216 | ||
| 217 | private boolean isActiveFileEditor( final FileEditorTab tab ) { | |
| 218 | return getActiveFileEditor() == tab; | |
| 219 | } | |
| 220 | ||
| 221 | private Path getDefaultPath() { | |
| 222 | final String filename = getDefaultFilename(); | |
| 223 | return (new File( filename )).toPath(); | |
| 224 | } | |
| 225 | ||
| 226 | private String getDefaultFilename() { | |
| 227 | return getSettings().getSetting( "file.default", "untitled.md" ); | |
| 228 | } | |
| 229 | ||
| 230 | /** | |
| 231 | * Called to add a new {@link FileEditorTab} to the tab pane. | |
| 232 | */ | |
| 233 | void newEditor() { | |
| 234 | final FileEditorTab tab = createFileEditor( getDefaultPath() ); | |
| 235 | ||
| 236 | getTabs().add( tab ); | |
| 237 | getSelectionModel().select( tab ); | |
| 238 | } | |
| 239 | ||
| 240 | void openFileDialog() { | |
| 241 | final String title = get( "Dialog.file.choose.open.title" ); | |
| 242 | final FileChooser dialog = createFileChooser( title ); | |
| 243 | final List<File> files = dialog.showOpenMultipleDialog( getWindow() ); | |
| 244 | ||
| 245 | if( files != null ) { | |
| 246 | openFiles( files ); | |
| 247 | } | |
| 248 | } | |
| 249 | ||
| 250 | /** | |
| 251 | * Opens the files into new editors, unless one of those files was a | |
| 252 | * definition file. The definition file is loaded into the definition pane, | |
| 253 | * but only the first one selected (multiple definition files will result in a | |
| 254 | * warning). | |
| 255 | * | |
| 256 | * @param files The list of non-definition files that the were requested to | |
| 257 | * open. | |
| 258 | */ | |
| 259 | private void openFiles( final List<File> files ) { | |
| 260 | final List<String> extensions = | |
| 261 | createExtensionFilter( DEFINITION ).getExtensions(); | |
| 262 | final var predicate = createFileTypePredicate( extensions ); | |
| 263 | ||
| 264 | // The user might have opened multiple definitions files. These will | |
| 265 | // be discarded from the text editable files. | |
| 266 | final var definitions | |
| 267 | = files.stream().filter( predicate ).collect( Collectors.toList() ); | |
| 268 | ||
| 269 | // Create a modifiable list to remove any definition files that were | |
| 270 | // opened. | |
| 271 | final var editors = new ArrayList<>( files ); | |
| 272 | ||
| 273 | if( !editors.isEmpty() ) { | |
| 274 | saveLastDirectory( editors.get( 0 ) ); | |
| 275 | } | |
| 276 | ||
| 277 | editors.removeAll( definitions ); | |
| 278 | ||
| 279 | // Open editor-friendly files (e.g,. Markdown, XML) in new tabs. | |
| 280 | if( !editors.isEmpty() ) { | |
| 281 | openEditors( editors, 0 ); | |
| 282 | } | |
| 283 | ||
| 284 | if( !definitions.isEmpty() ) { | |
| 285 | openDefinition( definitions.get( 0 ) ); | |
| 286 | } | |
| 287 | } | |
| 288 | ||
| 289 | private void openEditors( final List<File> files, final int activeIndex ) { | |
| 290 | final int fileTally = files.size(); | |
| 291 | final List<Tab> tabs = getTabs(); | |
| 292 | ||
| 293 | // Close single unmodified "Untitled" tab. | |
| 294 | if( tabs.size() == 1 ) { | |
| 295 | final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 )); | |
| 296 | ||
| 297 | if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | |
| 298 | closeEditor( fileEditor, false ); | |
| 299 | } | |
| 300 | } | |
| 301 | ||
| 302 | for( int i = 0; i < fileTally; i++ ) { | |
| 303 | final Path path = files.get( i ).toPath(); | |
| 304 | ||
| 305 | FileEditorTab fileEditorTab = findEditor( path ); | |
| 306 | ||
| 307 | // Only open new files. | |
| 308 | if( fileEditorTab == null ) { | |
| 309 | fileEditorTab = createFileEditor( path ); | |
| 310 | getTabs().add( fileEditorTab ); | |
| 311 | } | |
| 312 | ||
| 313 | // Select the first file in the list. | |
| 314 | if( i == activeIndex ) { | |
| 315 | getSelectionModel().select( fileEditorTab ); | |
| 316 | } | |
| 317 | } | |
| 318 | } | |
| 319 | ||
| 320 | /** | |
| 321 | * Returns a property that changes when a new definition file is opened. | |
| 322 | * | |
| 323 | * @return The path to a definition file that was opened. | |
| 324 | */ | |
| 325 | public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() { | |
| 326 | return getOnOpenDefinitionFile().getReadOnlyProperty(); | |
| 327 | } | |
| 328 | ||
| 329 | private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() { | |
| 330 | return mOpenDefinition; | |
| 331 | } | |
| 332 | ||
| 333 | /** | |
| 334 | * Called when the user has opened a definition file (using the file open | |
| 335 | * dialog box). This will replace the current set of definitions for the | |
| 336 | * active tab. | |
| 337 | * | |
| 338 | * @param definition The file to open. | |
| 339 | */ | |
| 340 | private void openDefinition( final File definition ) { | |
| 341 | // TODO: Prevent reading this file twice when a new text document is opened. | |
| 342 | // (might be a matter of checking the value first). | |
| 343 | getOnOpenDefinitionFile().set( definition.toPath() ); | |
| 344 | } | |
| 345 | ||
| 346 | /** | |
| 347 | * Called when the contents of the editor are to be saved. | |
| 348 | * | |
| 349 | * @param tab The tab containing content to save. | |
| 350 | * @return true The contents were saved (or needn't be saved). | |
| 351 | */ | |
| 352 | public boolean saveEditor( final FileEditorTab tab ) { | |
| 353 | if( tab == null || !tab.isModified() ) { | |
| 354 | return true; | |
| 355 | } | |
| 356 | ||
| 357 | return tab.getPath() == null ? saveEditorAs( tab ) : tab.save(); | |
| 358 | } | |
| 359 | ||
| 360 | /** | |
| 361 | * Opens the Save As dialog for the user to save the content under a new | |
| 362 | * path. | |
| 363 | * | |
| 364 | * @param tab The tab with contents to save. | |
| 365 | * @return true The contents were saved, or the tab was null. | |
| 366 | */ | |
| 367 | public boolean saveEditorAs( final FileEditorTab tab ) { | |
| 368 | if( tab == null ) { | |
| 369 | return true; | |
| 370 | } | |
| 371 | ||
| 372 | getSelectionModel().select( tab ); | |
| 373 | ||
| 374 | final FileChooser fileChooser = createFileChooser( get( | |
| 375 | "Dialog.file.choose.save.title" ) ); | |
| 376 | final File file = fileChooser.showSaveDialog( getWindow() ); | |
| 377 | if( file == null ) { | |
| 378 | return false; | |
| 379 | } | |
| 380 | ||
| 381 | saveLastDirectory( file ); | |
| 382 | tab.setPath( file.toPath() ); | |
| 383 | ||
| 384 | return tab.save(); | |
| 385 | } | |
| 386 | ||
| 387 | void saveAllEditors() { | |
| 388 | for( final FileEditorTab fileEditor : getAllEditors() ) { | |
| 389 | saveEditor( fileEditor ); | |
| 390 | } | |
| 391 | } | |
| 392 | ||
| 393 | /** | |
| 394 | * Answers whether the file has had modifications. ' | |
| 395 | * | |
| 396 | * @param tab THe tab to check for modifications. | |
| 397 | * @return false The file is unmodified. | |
| 398 | */ | |
| 399 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") | |
| 400 | boolean canCloseEditor( final FileEditorTab tab ) { | |
| 401 | final AtomicReference<Boolean> canClose = new AtomicReference<>(); | |
| 402 | canClose.set( true ); | |
| 403 | ||
| 404 | if( tab.isModified() ) { | |
| 405 | final Notification message = getNotifyService().createNotification( | |
| 406 | Messages.get( "Alert.file.close.title" ), | |
| 407 | Messages.get( "Alert.file.close.text" ), | |
| 408 | tab.getText() | |
| 409 | ); | |
| 410 | ||
| 411 | final Alert confirmSave = getNotifyService().createConfirmation( | |
| 412 | getWindow(), message ); | |
| 413 | ||
| 414 | final Optional<ButtonType> buttonType = confirmSave.showAndWait(); | |
| 415 | ||
| 416 | buttonType.ifPresent( | |
| 417 | save -> canClose.set( | |
| 418 | save == YES ? saveEditor( tab ) : save == ButtonType.NO | |
| 419 | ) | |
| 420 | ); | |
| 421 | } | |
| 422 | ||
| 423 | return canClose.get(); | |
| 424 | } | |
| 425 | ||
| 426 | boolean closeEditor( final FileEditorTab tab, final boolean save ) { | |
| 427 | if( tab == null ) { | |
| 428 | return true; | |
| 429 | } | |
| 430 | ||
| 431 | if( save ) { | |
| 432 | Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | |
| 433 | Event.fireEvent( tab, event ); | |
| 434 | ||
| 435 | if( event.isConsumed() ) { | |
| 436 | return false; | |
| 437 | } | |
| 438 | } | |
| 439 | ||
| 440 | getTabs().remove( tab ); | |
| 441 | ||
| 442 | if( tab.getOnClosed() != null ) { | |
| 443 | Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | |
| 444 | } | |
| 445 | ||
| 446 | return true; | |
| 447 | } | |
| 448 | ||
| 449 | boolean closeAllEditors() { | |
| 450 | final FileEditorTab[] allEditors = getAllEditors(); | |
| 451 | final FileEditorTab activeEditor = getActiveFileEditor(); | |
| 452 | ||
| 453 | // try to save active tab first because in case the user decides to cancel, | |
| 454 | // then it stays active | |
| 455 | if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | |
| 456 | return false; | |
| 457 | } | |
| 458 | ||
| 459 | // This should be called any time a tab changes. | |
| 460 | persistPreferences(); | |
| 461 | ||
| 462 | // save modified tabs | |
| 463 | for( int i = 0; i < allEditors.length; i++ ) { | |
| 464 | final FileEditorTab fileEditor = allEditors[ i ]; | |
| 465 | ||
| 466 | if( fileEditor == activeEditor ) { | |
| 467 | continue; | |
| 468 | } | |
| 469 | ||
| 470 | if( fileEditor.isModified() ) { | |
| 471 | // activate the modified tab to make its modified content visible to | |
| 472 | // the user | |
| 473 | getSelectionModel().select( i ); | |
| 474 | ||
| 475 | if( !canCloseEditor( fileEditor ) ) { | |
| 476 | return false; | |
| 477 | } | |
| 478 | } | |
| 479 | } | |
| 480 | ||
| 481 | // Close all tabs. | |
| 482 | for( final FileEditorTab fileEditor : allEditors ) { | |
| 483 | if( !closeEditor( fileEditor, false ) ) { | |
| 484 | return false; | |
| 485 | } | |
| 486 | } | |
| 487 | ||
| 488 | return getTabs().isEmpty(); | |
| 489 | } | |
| 490 | ||
| 491 | private FileEditorTab[] getAllEditors() { | |
| 492 | final ObservableList<Tab> tabs = getTabs(); | |
| 493 | final int length = tabs.size(); | |
| 494 | final FileEditorTab[] allEditors = new FileEditorTab[ length ]; | |
| 495 | ||
| 496 | for( int i = 0; i < length; i++ ) { | |
| 497 | allEditors[ i ] = (FileEditorTab) tabs.get( i ); | |
| 498 | } | |
| 499 | ||
| 500 | return allEditors; | |
| 501 | } | |
| 502 | ||
| 503 | /** | |
| 504 | * Returns the file editor tab that has the given path. | |
| 505 | * | |
| 506 | * @return null No file editor tab for the given path was found. | |
| 507 | */ | |
| 508 | private FileEditorTab findEditor( final Path path ) { | |
| 509 | for( final Tab tab : getTabs() ) { | |
| 510 | final FileEditorTab fileEditor = (FileEditorTab) tab; | |
| 511 | ||
| 512 | if( fileEditor.isPath( path ) ) { | |
| 513 | return fileEditor; | |
| 514 | } | |
| 515 | } | |
| 516 | ||
| 517 | return null; | |
| 518 | } | |
| 519 | ||
| 520 | private FileChooser createFileChooser( String title ) { | |
| 521 | final FileChooser fileChooser = new FileChooser(); | |
| 522 | ||
| 523 | fileChooser.setTitle( title ); | |
| 524 | fileChooser.getExtensionFilters().addAll( | |
| 525 | createExtensionFilters() ); | |
| 526 | ||
| 527 | final String lastDirectory = getPreferences().get( "lastDirectory", null ); | |
| 528 | File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | |
| 529 | ||
| 530 | if( !file.isDirectory() ) { | |
| 531 | file = new File( "." ); | |
| 532 | } | |
| 533 | ||
| 534 | fileChooser.setInitialDirectory( file ); | |
| 535 | return fileChooser; | |
| 536 | } | |
| 537 | ||
| 538 | private List<ExtensionFilter> createExtensionFilters() { | |
| 539 | final List<ExtensionFilter> list = new ArrayList<>(); | |
| 540 | ||
| 541 | // TODO: Return a list of all properties that match the filter prefix. | |
| 542 | // This will allow dynamic filters to be added and removed just by | |
| 543 | // updating the properties file. | |
| 544 | list.add( createExtensionFilter( ALL ) ); | |
| 545 | list.add( createExtensionFilter( SOURCE ) ); | |
| 546 | list.add( createExtensionFilter( DEFINITION ) ); | |
| 547 | list.add( createExtensionFilter( XML ) ); | |
| 548 | return list; | |
| 549 | } | |
| 550 | ||
| 551 | /** | |
| 552 | * Returns a filter for file name extensions recognized by the application | |
| 553 | * that can be opened by the user. | |
| 554 | * | |
| 555 | * @param filetype Used to find the globbing pattern for extensions. | |
| 556 | * @return A filename filter suitable for use by a FileDialog instance. | |
| 557 | */ | |
| 558 | private ExtensionFilter createExtensionFilter( final FileType filetype ) { | |
| 559 | final String tKey = String.format( "%s.title.%s", | |
| 560 | FILTER_EXTENSION_TITLES, | |
| 561 | filetype ); | |
| 562 | final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype ); | |
| 563 | ||
| 564 | return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); | |
| 565 | } | |
| 566 | ||
| 567 | private void saveLastDirectory( final File file ) { | |
| 568 | getPreferences().put( "lastDirectory", file.getParent() ); | |
| 569 | } | |
| 570 | ||
| 571 | public void initPreferences() { | |
| 572 | int activeIndex = 0; | |
| 573 | ||
| 574 | final Preferences preferences = getPreferences(); | |
| 575 | final String[] fileNames = Utils.getPrefsStrings( preferences, "file" ); | |
| 576 | final String activeFileName = preferences.get( "activeFile", null ); | |
| 577 | ||
| 578 | final List<File> files = new ArrayList<>( fileNames.length ); | |
| 579 | ||
| 580 | for( final String fileName : fileNames ) { | |
| 581 | final File file = new File( fileName ); | |
| 582 | ||
| 583 | if( file.exists() ) { | |
| 584 | files.add( file ); | |
| 585 | ||
| 586 | if( fileName.equals( activeFileName ) ) { | |
| 587 | activeIndex = files.size() - 1; | |
| 588 | } | |
| 589 | } | |
| 590 | } | |
| 591 | ||
| 592 | if( files.isEmpty() ) { | |
| 593 | newEditor(); | |
| 594 | } | |
| 595 | else { | |
| 596 | openEditors( files, activeIndex ); | |
| 597 | } | |
| 598 | } | |
| 599 | ||
| 600 | public void persistPreferences() { | |
| 601 | final var allEditors = getTabs(); | |
| 602 | final List<String> fileNames = new ArrayList<>( allEditors.size() ); | |
| 603 | ||
| 604 | for( final var tab : allEditors ) { | |
| 605 | final var fileEditor = (FileEditorTab) tab; | |
| 606 | final var filePath = fileEditor.getPath(); | |
| 607 | ||
| 608 | if( filePath != null ) { | |
| 609 | fileNames.add( filePath.toString() ); | |
| 610 | } | |
| 611 | } | |
| 612 | ||
| 613 | final var preferences = getPreferences(); | |
| 614 | Utils.putPrefsStrings( preferences, | |
| 615 | "file", | |
| 616 | fileNames.toArray( new String[ 0 ] ) ); | |
| 617 | ||
| 618 | final var activeEditor = getActiveFileEditor(); | |
| 619 | final var filePath = activeEditor == null ? null : activeEditor.getPath(); | |
| 620 | ||
| 621 | if( filePath == null ) { | |
| 622 | preferences.remove( "activeFile" ); | |
| 623 | } | |
| 624 | else { | |
| 625 | preferences.put( "activeFile", filePath.toString() ); | |
| 626 | } | |
| 627 | } | |
| 628 | ||
| 629 | private List<String> getExtensions( final String key ) { | |
| 630 | return getSettings().getStringSettingList( key ); | |
| 631 | } | |
| 632 | ||
| 633 | private Notifier getNotifyService() { | |
| 634 | return sNotifier; | |
| 635 | } | |
| 636 | ||
| 637 | private Settings getSettings() { | |
| 638 | return SETTINGS; | |
| 639 | } | |
| 640 | ||
| 641 | protected Options getOptions() { | |
| 642 | return sOptions; | |
| 643 | } | |
| 644 | ||
| 645 | private Window getWindow() { | |
| 646 | return getScene().getWindow(); | |
| 647 | } | |
| 648 | ||
| 649 | private Preferences getPreferences() { | |
| 650 | return getOptions().getState(); | |
| 651 | } | |
| 652 | } | |
| 1 | 653 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents different file type classifications. These are high-level mappings | |
| 32 | * that correspond to the list of glob patterns found within {@code | |
| 33 | * settings.properties}. | |
| 34 | */ | |
| 35 | public enum FileType { | |
| 36 | ||
| 37 | ALL( "all" ), | |
| 38 | RMARKDOWN( "rmarkdown" ), | |
| 39 | RXML( "rxml" ), | |
| 40 | SOURCE( "source" ), | |
| 41 | DEFINITION( "definition" ), | |
| 42 | XML( "xml" ), | |
| 43 | CSV( "csv" ), | |
| 44 | JSON( "json" ), | |
| 45 | TOML( "toml" ), | |
| 46 | YAML( "yaml" ), | |
| 47 | PROPERTIES( "properties" ), | |
| 48 | UNKNOWN( "unknown" ); | |
| 49 | ||
| 50 | private final String mType; | |
| 51 | ||
| 52 | /** | |
| 53 | * Default constructor for enumerated file type. | |
| 54 | * | |
| 55 | * @param type Human-readable name for the file type. | |
| 56 | */ | |
| 57 | FileType( final String type ) { | |
| 58 | mType = type; | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Returns the file type that corresponds to the given string. | |
| 63 | * | |
| 64 | * @param type The string to compare against this enumeration of file types. | |
| 65 | * @return The corresponding File Type for the given string. | |
| 66 | * @throws IllegalArgumentException Type not found. | |
| 67 | */ | |
| 68 | public static FileType from( final String type ) { | |
| 69 | for( final FileType fileType : FileType.values() ) { | |
| 70 | if( fileType.isType( type ) ) { | |
| 71 | return fileType; | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | throw new IllegalArgumentException( type ); | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * Answers whether this file type matches the given string, case insensitive | |
| 80 | * comparison. | |
| 81 | * | |
| 82 | * @param type Presumably a file name extension to check against. | |
| 83 | * @return true The given extension corresponds to this enumerated type. | |
| 84 | */ | |
| 85 | public boolean isType( final String type ) { | |
| 86 | return getType().equalsIgnoreCase( type ); | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * Returns the human-readable name for the file type. | |
| 91 | * | |
| 92 | * @return A non-null instance. | |
| 93 | */ | |
| 94 | private String getType() { | |
| 95 | return mType; | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Returns the lowercase version of the file name extension. | |
| 100 | * | |
| 101 | * @return The file name, in lower case. | |
| 102 | */ | |
| 103 | @Override | |
| 104 | public String toString() { | |
| 105 | return getType(); | |
| 106 | } | |
| 107 | } | |
| 1 | 108 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import java.io.IOException; | |
| 31 | import java.io.InputStream; | |
| 32 | import java.util.Calendar; | |
| 33 | import java.util.Properties; | |
| 34 | ||
| 35 | import static com.keenwrite.Bootstrap.APP_TITLE; | |
| 36 | import static java.lang.String.format; | |
| 37 | ||
| 38 | /** | |
| 39 | * Launches the application using the {@link Main} class. | |
| 40 | * | |
| 41 | * <p> | |
| 42 | * This is required until modules are implemented, which may never happen | |
| 43 | * because the application should be ported away from Java and JavaFX. | |
| 44 | * </p> | |
| 45 | */ | |
| 46 | public class Launcher { | |
| 47 | /** | |
| 48 | * Delegates to the application entry point. | |
| 49 | * | |
| 50 | * @param args Command-line arguments. | |
| 51 | */ | |
| 52 | public static void main( final String[] args ) throws IOException { | |
| 53 | showAppInfo(); | |
| 54 | Main.main( args ); | |
| 55 | } | |
| 56 | ||
| 57 | @SuppressWarnings("RedundantStringFormatCall") | |
| 58 | private static void showAppInfo() throws IOException { | |
| 59 | out( format( "%s version %s", APP_TITLE, getVersion() ) ); | |
| 60 | out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) ); | |
| 61 | out( format( "Portions copyright 2020 Karl Tauber." ) ); | |
| 62 | } | |
| 63 | ||
| 64 | private static void out( final String s ) { | |
| 65 | System.out.println( s ); | |
| 66 | } | |
| 67 | ||
| 68 | private static String getVersion() throws IOException { | |
| 69 | final Properties properties = loadProperties( "app.properties" ); | |
| 70 | return properties.getProperty( "application.version" ); | |
| 71 | } | |
| 72 | ||
| 73 | private static String getYear() { | |
| 74 | return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) ); | |
| 75 | } | |
| 76 | ||
| 77 | @SuppressWarnings("SameParameterValue") | |
| 78 | private static Properties loadProperties( final String resource ) | |
| 79 | throws IOException { | |
| 80 | final Properties properties = new Properties(); | |
| 81 | properties.load( getResourceAsStream( getResourceName( resource ) ) ); | |
| 82 | return properties; | |
| 83 | } | |
| 84 | ||
| 85 | private static String getResourceName( final String resource ) { | |
| 86 | return format( "%s/%s", getPackagePath(), resource ); | |
| 87 | } | |
| 88 | ||
| 89 | private static String getPackagePath() { | |
| 90 | return Launcher.class.getPackageName().replace( '.', '/' ); | |
| 91 | } | |
| 92 | ||
| 93 | private static InputStream getResourceAsStream( final String resource ) { | |
| 94 | return Launcher.class.getClassLoader().getResourceAsStream( resource ); | |
| 95 | } | |
| 96 | } | |
| 1 | 97 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.keenwrite.preferences.FilePreferencesFactory; | |
| 31 | import com.keenwrite.service.Options; | |
| 32 | import com.keenwrite.service.Snitch; | |
| 33 | import com.keenwrite.util.ResourceWalker; | |
| 34 | import com.keenwrite.util.StageState; | |
| 35 | import javafx.application.Application; | |
| 36 | import javafx.scene.Scene; | |
| 37 | import javafx.scene.image.Image; | |
| 38 | import javafx.stage.Stage; | |
| 39 | ||
| 40 | import java.awt.*; | |
| 41 | import java.io.FileInputStream; | |
| 42 | import java.io.IOException; | |
| 43 | import java.io.InputStream; | |
| 44 | import java.net.URI; | |
| 45 | import java.util.Map; | |
| 46 | import java.util.logging.LogManager; | |
| 47 | ||
| 48 | import static com.keenwrite.Bootstrap.APP_TITLE; | |
| 49 | import static com.keenwrite.Constants.*; | |
| 50 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 51 | import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment; | |
| 52 | import static java.awt.font.TextAttribute.*; | |
| 53 | import static javafx.scene.input.KeyCode.F11; | |
| 54 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 55 | ||
| 56 | /** | |
| 57 | * Application entry point. The application allows users to edit Markdown | |
| 58 | * files and see a real-time preview of the edits. | |
| 59 | */ | |
| 60 | public final class Main extends Application { | |
| 61 | ||
| 62 | static { | |
| 63 | // Suppress logging to standard output. | |
| 64 | LogManager.getLogManager().reset(); | |
| 65 | ||
| 66 | // Suppress logging to standard error. | |
| 67 | System.err.close(); | |
| 68 | } | |
| 69 | ||
| 70 | private final Options mOptions = Services.load( Options.class ); | |
| 71 | private final Snitch mSnitch = Services.load( Snitch.class ); | |
| 72 | ||
| 73 | private final Thread mSnitchThread = new Thread( getSnitch() ); | |
| 74 | private final MainWindow mMainWindow = new MainWindow(); | |
| 75 | ||
| 76 | @SuppressWarnings({"FieldCanBeLocal"}) | |
| 77 | private StageState mStageState; | |
| 78 | ||
| 79 | /** | |
| 80 | * Application entry point. | |
| 81 | * | |
| 82 | * @param args Command-line arguments. | |
| 83 | */ | |
| 84 | public static void main( final String[] args ) { | |
| 85 | initPreferences(); | |
| 86 | initFonts(); | |
| 87 | launch( args ); | |
| 88 | } | |
| 89 | ||
| 90 | /** | |
| 91 | * JavaFX entry point. | |
| 92 | * | |
| 93 | * @param stage The primary application stage. | |
| 94 | */ | |
| 95 | @Override | |
| 96 | public void start( final Stage stage ) { | |
| 97 | initState( stage ); | |
| 98 | initStage( stage ); | |
| 99 | initSnitch(); | |
| 100 | ||
| 101 | stage.show(); | |
| 102 | ||
| 103 | // After the stage is visible, the panel dimensions are | |
| 104 | // known, which allows scaling images to fit the preview panel. | |
| 105 | getMainWindow().init(); | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * This needs to run before the windowing system kicks in, otherwise the | |
| 110 | * fonts will not be found. | |
| 111 | */ | |
| 112 | @SuppressWarnings({"rawtypes", "unchecked"}) | |
| 113 | public static void initFonts() { | |
| 114 | final var ge = getLocalGraphicsEnvironment(); | |
| 115 | ||
| 116 | try { | |
| 117 | ResourceWalker.walk( | |
| 118 | FONT_DIRECTORY, path -> { | |
| 119 | final var uri = path.toUri(); | |
| 120 | final var filename = path.toString(); | |
| 121 | ||
| 122 | try( final var is = openFont( uri, filename ) ) { | |
| 123 | final var font = Font.createFont( Font.TRUETYPE_FONT, is ); | |
| 124 | final Map attributes = font.getAttributes(); | |
| 125 | ||
| 126 | attributes.put( LIGATURES, LIGATURES_ON ); | |
| 127 | attributes.put( KERNING, KERNING_ON ); | |
| 128 | ge.registerFont( font.deriveFont( attributes ) ); | |
| 129 | } catch( final Exception e ) { | |
| 130 | alert( e ); | |
| 131 | } | |
| 132 | } | |
| 133 | ); | |
| 134 | } catch( final Exception e ) { | |
| 135 | alert( e ); | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | private static InputStream openFont( final URI uri, final String filename ) | |
| 140 | throws IOException { | |
| 141 | return uri.getScheme().equals( "jar" ) | |
| 142 | ? Main.class.getResourceAsStream( filename ) | |
| 143 | : new FileInputStream( filename ); | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Sets the factory used for reading user preferences. | |
| 148 | */ | |
| 149 | private static void initPreferences() { | |
| 150 | System.setProperty( | |
| 151 | "java.util.prefs.PreferencesFactory", | |
| 152 | FilePreferencesFactory.class.getName() | |
| 153 | ); | |
| 154 | } | |
| 155 | ||
| 156 | private void initState( final Stage stage ) { | |
| 157 | mStageState = new StageState( stage, getOptions().getState() ); | |
| 158 | } | |
| 159 | ||
| 160 | private void initStage( final Stage stage ) { | |
| 161 | stage.getIcons().addAll( | |
| 162 | createImage( FILE_LOGO_16 ), | |
| 163 | createImage( FILE_LOGO_32 ), | |
| 164 | createImage( FILE_LOGO_128 ), | |
| 165 | createImage( FILE_LOGO_256 ), | |
| 166 | createImage( FILE_LOGO_512 ) ); | |
| 167 | stage.setTitle( APP_TITLE ); | |
| 168 | stage.setScene( getScene() ); | |
| 169 | ||
| 170 | stage.addEventHandler( KEY_PRESSED, event -> { | |
| 171 | if( F11.equals( event.getCode() ) ) { | |
| 172 | stage.setFullScreen( !stage.isFullScreen() ); | |
| 173 | } | |
| 174 | } ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Watch for file system changes. | |
| 179 | */ | |
| 180 | private void initSnitch() { | |
| 181 | getSnitchThread().start(); | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Stops the snitch service, if its running. | |
| 186 | * | |
| 187 | * @throws InterruptedException Couldn't stop the snitch thread. | |
| 188 | */ | |
| 189 | @Override | |
| 190 | public void stop() throws InterruptedException { | |
| 191 | getSnitch().stop(); | |
| 192 | ||
| 193 | final Thread thread = getSnitchThread(); | |
| 194 | thread.interrupt(); | |
| 195 | thread.join(); | |
| 196 | } | |
| 197 | ||
| 198 | private Snitch getSnitch() { | |
| 199 | return mSnitch; | |
| 200 | } | |
| 201 | ||
| 202 | private Thread getSnitchThread() { | |
| 203 | return mSnitchThread; | |
| 204 | } | |
| 205 | ||
| 206 | private Options getOptions() { | |
| 207 | return mOptions; | |
| 208 | } | |
| 209 | ||
| 210 | private MainWindow getMainWindow() { | |
| 211 | return mMainWindow; | |
| 212 | } | |
| 213 | ||
| 214 | private Scene getScene() { | |
| 215 | return getMainWindow().getScene(); | |
| 216 | } | |
| 217 | ||
| 218 | private Image createImage( final String filename ) { | |
| 219 | return new Image( filename ); | |
| 220 | } | |
| 221 | ||
| 222 | /** | |
| 223 | * This is here to suppress an IDE warning, the method is not used. | |
| 224 | */ | |
| 225 | public StageState getStageState() { | |
| 226 | return mStageState; | |
| 227 | } | |
| 228 | } | |
| 1 | 229 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.dlsc.preferencesfx.PreferencesFxEvent; | |
| 31 | import com.keenwrite.definition.DefinitionFactory; | |
| 32 | import com.keenwrite.definition.DefinitionPane; | |
| 33 | import com.keenwrite.definition.DefinitionSource; | |
| 34 | import com.keenwrite.definition.MapInterpolator; | |
| 35 | import com.keenwrite.definition.yaml.YamlDefinitionSource; | |
| 36 | import com.keenwrite.editors.DefinitionNameInjector; | |
| 37 | import com.keenwrite.editors.EditorPane; | |
| 38 | import com.keenwrite.editors.markdown.MarkdownEditorPane; | |
| 39 | import com.keenwrite.preferences.UserPreferences; | |
| 40 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 41 | import com.keenwrite.exceptions.MissingFileException; | |
| 42 | import com.keenwrite.processors.HtmlPreviewProcessor; | |
| 43 | import com.keenwrite.processors.Processor; | |
| 44 | import com.keenwrite.processors.ProcessorFactory; | |
| 45 | import com.keenwrite.service.Options; | |
| 46 | import com.keenwrite.service.Snitch; | |
| 47 | import com.keenwrite.spelling.api.SpellCheckListener; | |
| 48 | import com.keenwrite.spelling.api.SpellChecker; | |
| 49 | import com.keenwrite.spelling.impl.PermissiveSpeller; | |
| 50 | import com.keenwrite.spelling.impl.SymSpellSpeller; | |
| 51 | import com.keenwrite.util.Action; | |
| 52 | import com.keenwrite.util.ActionBuilder; | |
| 53 | import com.keenwrite.util.ActionUtils; | |
| 54 | import com.vladsch.flexmark.parser.Parser; | |
| 55 | import com.vladsch.flexmark.util.ast.NodeVisitor; | |
| 56 | import com.vladsch.flexmark.util.ast.VisitHandler; | |
| 57 | import javafx.beans.binding.Bindings; | |
| 58 | import javafx.beans.binding.BooleanBinding; | |
| 59 | import javafx.beans.property.BooleanProperty; | |
| 60 | import javafx.beans.property.SimpleBooleanProperty; | |
| 61 | import javafx.beans.value.ChangeListener; | |
| 62 | import javafx.beans.value.ObservableBooleanValue; | |
| 63 | import javafx.beans.value.ObservableValue; | |
| 64 | import javafx.collections.ListChangeListener.Change; | |
| 65 | import javafx.collections.ObservableList; | |
| 66 | import javafx.event.Event; | |
| 67 | import javafx.event.EventHandler; | |
| 68 | import javafx.geometry.Pos; | |
| 69 | import javafx.scene.Node; | |
| 70 | import javafx.scene.Scene; | |
| 71 | import javafx.scene.control.*; | |
| 72 | import javafx.scene.image.ImageView; | |
| 73 | import javafx.scene.input.Clipboard; | |
| 74 | import javafx.scene.input.ClipboardContent; | |
| 75 | import javafx.scene.input.KeyEvent; | |
| 76 | import javafx.scene.layout.BorderPane; | |
| 77 | import javafx.scene.layout.VBox; | |
| 78 | import javafx.scene.text.Text; | |
| 79 | import javafx.stage.Window; | |
| 80 | import javafx.stage.WindowEvent; | |
| 81 | import javafx.util.Duration; | |
| 82 | import org.apache.commons.lang3.SystemUtils; | |
| 83 | import org.controlsfx.control.StatusBar; | |
| 84 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 85 | import org.fxmisc.richtext.model.StyleSpansBuilder; | |
| 86 | import org.reactfx.value.Val; | |
| 87 | ||
| 88 | import java.io.BufferedReader; | |
| 89 | import java.io.InputStreamReader; | |
| 90 | import java.nio.file.Path; | |
| 91 | import java.util.*; | |
| 92 | import java.util.concurrent.atomic.AtomicInteger; | |
| 93 | import java.util.function.Consumer; | |
| 94 | import java.util.function.Function; | |
| 95 | import java.util.prefs.Preferences; | |
| 96 | import java.util.stream.Collectors; | |
| 97 | ||
| 98 | import static com.keenwrite.Bootstrap.APP_TITLE; | |
| 99 | import static com.keenwrite.Constants.*; | |
| 100 | import static com.keenwrite.Messages.get; | |
| 101 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 102 | import static com.keenwrite.util.StageState.*; | |
| 103 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 104 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 105 | import static java.util.Collections.emptyList; | |
| 106 | import static java.util.Collections.singleton; | |
| 107 | import static javafx.application.Platform.runLater; | |
| 108 | import static javafx.event.Event.fireEvent; | |
| 109 | import static javafx.scene.control.Alert.AlertType.INFORMATION; | |
| 110 | import static javafx.scene.input.KeyCode.ENTER; | |
| 111 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 112 | import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward; | |
| 113 | ||
| 114 | /** | |
| 115 | * Main window containing a tab pane in the center for file editors. | |
| 116 | */ | |
| 117 | public class MainWindow implements Observer { | |
| 118 | /** | |
| 119 | * The {@code OPTIONS} variable must be declared before all other variables | |
| 120 | * to prevent subsequent initializations from failing due to missing user | |
| 121 | * preferences. | |
| 122 | */ | |
| 123 | private static final Options sOptions = Services.load( Options.class ); | |
| 124 | private static final Snitch SNITCH = Services.load( Snitch.class ); | |
| 125 | ||
| 126 | private final Scene mScene; | |
| 127 | private final StatusBar mStatusBar; | |
| 128 | private final Text mLineNumberText; | |
| 129 | private final TextField mFindTextField; | |
| 130 | private final SpellChecker mSpellChecker; | |
| 131 | ||
| 132 | private final Object mMutex = new Object(); | |
| 133 | ||
| 134 | /** | |
| 135 | * Prevents re-instantiation of processing classes. | |
| 136 | */ | |
| 137 | private final Map<FileEditorTab, Processor<String>> mProcessors = | |
| 138 | new HashMap<>(); | |
| 139 | ||
| 140 | private final Map<String, String> mResolvedMap = | |
| 141 | new HashMap<>( DEFAULT_MAP_SIZE ); | |
| 142 | ||
| 143 | private final EventHandler<PreferencesFxEvent> mRPreferencesListener = | |
| 144 | event -> rerender(); | |
| 145 | ||
| 146 | /** | |
| 147 | * Called when the definition data is changed. | |
| 148 | */ | |
| 149 | private final EventHandler<TreeItem.TreeModificationEvent<Event>> | |
| 150 | mTreeHandler = event -> { | |
| 151 | exportDefinitions( getDefinitionPath() ); | |
| 152 | interpolateResolvedMap(); | |
| 153 | rerender(); | |
| 154 | }; | |
| 155 | ||
| 156 | /** | |
| 157 | * Called to inject the selected item when the user presses ENTER in the | |
| 158 | * definition pane. | |
| 159 | */ | |
| 160 | private final EventHandler<? super KeyEvent> mDefinitionKeyHandler = | |
| 161 | event -> { | |
| 162 | if( event.getCode() == ENTER ) { | |
| 163 | getDefinitionNameInjector().injectSelectedItem(); | |
| 164 | } | |
| 165 | }; | |
| 166 | ||
| 167 | private final ChangeListener<Integer> mCaretPositionListener = | |
| 168 | ( observable, oldPosition, newPosition ) -> { | |
| 169 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 170 | final EditorPane pane = tab.getEditorPane(); | |
| 171 | final StyleClassedTextArea editor = pane.getEditor(); | |
| 172 | ||
| 173 | getLineNumberText().setText( | |
| 174 | get( STATUS_BAR_LINE, | |
| 175 | editor.getCurrentParagraph() + 1, | |
| 176 | editor.getParagraphs().size(), | |
| 177 | editor.getCaretPosition() | |
| 178 | ) | |
| 179 | ); | |
| 180 | }; | |
| 181 | ||
| 182 | private final ChangeListener<Integer> mCaretParagraphListener = | |
| 183 | ( observable, oldIndex, newIndex ) -> | |
| 184 | scrollToParagraph( newIndex, true ); | |
| 185 | ||
| 186 | private DefinitionSource mDefinitionSource = createDefaultDefinitionSource(); | |
| 187 | private final DefinitionPane mDefinitionPane = createDefinitionPane(); | |
| 188 | private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane(); | |
| 189 | private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane( | |
| 190 | mCaretPositionListener, | |
| 191 | mCaretParagraphListener ); | |
| 192 | ||
| 193 | /** | |
| 194 | * Listens on the definition pane for double-click events. | |
| 195 | */ | |
| 196 | private final DefinitionNameInjector mDefinitionNameInjector | |
| 197 | = new DefinitionNameInjector( mDefinitionPane ); | |
| 198 | ||
| 199 | public MainWindow() { | |
| 200 | mStatusBar = createStatusBar(); | |
| 201 | mLineNumberText = createLineNumberText(); | |
| 202 | mFindTextField = createFindTextField(); | |
| 203 | mScene = createScene(); | |
| 204 | mSpellChecker = createSpellChecker(); | |
| 205 | ||
| 206 | // Add the close request listener before the window is shown. | |
| 207 | initLayout(); | |
| 208 | StatusBarNotifier.setStatusBar( mStatusBar ); | |
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Called after the stage is shown. | |
| 213 | */ | |
| 214 | public void init() { | |
| 215 | initFindInput(); | |
| 216 | initSnitch(); | |
| 217 | initDefinitionListener(); | |
| 218 | initTabAddedListener(); | |
| 219 | initTabChangedListener(); | |
| 220 | initPreferences(); | |
| 221 | initVariableNameInjector(); | |
| 222 | } | |
| 223 | ||
| 224 | private void initLayout() { | |
| 225 | final var scene = getScene(); | |
| 226 | ||
| 227 | scene.getStylesheets().add( STYLESHEET_SCENE ); | |
| 228 | scene.windowProperty().addListener( | |
| 229 | ( unused, oldWindow, newWindow ) -> | |
| 230 | newWindow.setOnCloseRequest( | |
| 231 | e -> { | |
| 232 | if( !getFileEditorPane().closeAllEditors() ) { | |
| 233 | e.consume(); | |
| 234 | } | |
| 235 | } | |
| 236 | ) | |
| 237 | ); | |
| 238 | } | |
| 239 | ||
| 240 | /** | |
| 241 | * Initialize the find input text field to listen on F3, ENTER, and | |
| 242 | * ESCAPE key presses. | |
| 243 | */ | |
| 244 | private void initFindInput() { | |
| 245 | final TextField input = getFindTextField(); | |
| 246 | ||
| 247 | input.setOnKeyPressed( ( KeyEvent event ) -> { | |
| 248 | switch( event.getCode() ) { | |
| 249 | case F3: | |
| 250 | case ENTER: | |
| 251 | editFindNext(); | |
| 252 | break; | |
| 253 | case F: | |
| 254 | if( !event.isControlDown() ) { | |
| 255 | break; | |
| 256 | } | |
| 257 | case ESCAPE: | |
| 258 | getStatusBar().setGraphic( null ); | |
| 259 | getActiveFileEditorTab().getEditorPane().requestFocus(); | |
| 260 | break; | |
| 261 | } | |
| 262 | } ); | |
| 263 | ||
| 264 | // Remove when the input field loses focus. | |
| 265 | input.focusedProperty().addListener( | |
| 266 | ( focused, oldFocus, newFocus ) -> { | |
| 267 | if( !newFocus ) { | |
| 268 | getStatusBar().setGraphic( null ); | |
| 269 | } | |
| 270 | } | |
| 271 | ); | |
| 272 | } | |
| 273 | ||
| 274 | /** | |
| 275 | * Watch for changes to external files. In particular, this awaits | |
| 276 | * modifications to any XSL files associated with XML files being edited. | |
| 277 | * When | |
| 278 | * an XSL file is modified (external to the application), the snitch's ears | |
| 279 | * perk up and the file is reloaded. This keeps the XSL transformation up to | |
| 280 | * date with what's on the file system. | |
| 281 | */ | |
| 282 | private void initSnitch() { | |
| 283 | SNITCH.addObserver( this ); | |
| 284 | } | |
| 285 | ||
| 286 | /** | |
| 287 | * Listen for {@link FileEditorTabPane} to receive open definition file | |
| 288 | * event. | |
| 289 | */ | |
| 290 | private void initDefinitionListener() { | |
| 291 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 292 | ( final ObservableValue<? extends Path> file, | |
| 293 | final Path oldPath, final Path newPath ) -> { | |
| 294 | openDefinitions( newPath ); | |
| 295 | rerender(); | |
| 296 | } | |
| 297 | ); | |
| 298 | } | |
| 299 | ||
| 300 | /** | |
| 301 | * Re-instantiates all processors then re-renders the active tab. This | |
| 302 | * will refresh the resolved map, force R to re-initialize, and brute-force | |
| 303 | * XSLT file reloads. | |
| 304 | */ | |
| 305 | private void rerender() { | |
| 306 | runLater( | |
| 307 | () -> { | |
| 308 | resetProcessors(); | |
| 309 | renderActiveTab(); | |
| 310 | } | |
| 311 | ); | |
| 312 | } | |
| 313 | ||
| 314 | /** | |
| 315 | * When tabs are added, hook the various change listeners onto the new | |
| 316 | * tab sothat the preview pane refreshes as necessary. | |
| 317 | */ | |
| 318 | private void initTabAddedListener() { | |
| 319 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 320 | ||
| 321 | // Make sure the text processor kicks off when new files are opened. | |
| 322 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 323 | ||
| 324 | // Update the preview pane on tab changes. | |
| 325 | tabs.addListener( | |
| 326 | ( final Change<? extends Tab> change ) -> { | |
| 327 | while( change.next() ) { | |
| 328 | if( change.wasAdded() ) { | |
| 329 | // Multiple tabs can be added simultaneously. | |
| 330 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 331 | final FileEditorTab tab = (FileEditorTab) newTab; | |
| 332 | ||
| 333 | initTextChangeListener( tab ); | |
| 334 | initScrollEventListener( tab ); | |
| 335 | initSpellCheckListener( tab ); | |
| 336 | // initSyntaxListener( tab ); | |
| 337 | } | |
| 338 | } | |
| 339 | } | |
| 340 | } | |
| 341 | ); | |
| 342 | } | |
| 343 | ||
| 344 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 345 | tab.addTextChangeListener( | |
| 346 | ( __, ov, nv ) -> { | |
| 347 | process( tab ); | |
| 348 | scrollToParagraph( getCurrentParagraphIndex() ); | |
| 349 | } | |
| 350 | ); | |
| 351 | } | |
| 352 | ||
| 353 | private void initScrollEventListener( final FileEditorTab tab ) { | |
| 354 | final var scrollPane = tab.getScrollPane(); | |
| 355 | final var scrollBar = getPreviewPane().getVerticalScrollBar(); | |
| 356 | ||
| 357 | addShowListener( scrollPane, ( __ ) -> { | |
| 358 | final var handler = new ScrollEventHandler( scrollPane, scrollBar ); | |
| 359 | handler.enabledProperty().bind( tab.selectedProperty() ); | |
| 360 | } ); | |
| 361 | } | |
| 362 | ||
| 363 | /** | |
| 364 | * Listen for changes to the any particular paragraph and perform a quick | |
| 365 | * spell check upon it. The style classes in the editor will be changed to | |
| 366 | * mark any spelling mistakes in the paragraph. The user may then interact | |
| 367 | * with any misspelled word (i.e., any piece of text that is marked) to | |
| 368 | * revise the spelling. | |
| 369 | * | |
| 370 | * @param tab The tab to spellcheck. | |
| 371 | */ | |
| 372 | private void initSpellCheckListener( final FileEditorTab tab ) { | |
| 373 | final var editor = tab.getEditorPane().getEditor(); | |
| 374 | ||
| 375 | // When the editor first appears, run a full spell check. This allows | |
| 376 | // spell checking while typing to be restricted to the active paragraph, | |
| 377 | // which is usually substantially smaller than the whole document. | |
| 378 | addShowListener( | |
| 379 | editor, ( __ ) -> spellcheck( editor, editor.getText() ) | |
| 380 | ); | |
| 381 | ||
| 382 | // Use the plain text changes so that notifications of style changes | |
| 383 | // are suppressed. Checking against the identity ensures that only | |
| 384 | // new text additions or deletions trigger proofreading. | |
| 385 | editor.plainTextChanges() | |
| 386 | .filter( p -> !p.isIdentity() ).subscribe( change -> { | |
| 387 | ||
| 388 | // Only perform a spell check on the current paragraph. The | |
| 389 | // entire document is processed once, when opened. | |
| 390 | final var offset = change.getPosition(); | |
| 391 | final var position = editor.offsetToPosition( offset, Forward ); | |
| 392 | final var paraId = position.getMajor(); | |
| 393 | final var paragraph = editor.getParagraph( paraId ); | |
| 394 | final var text = paragraph.getText(); | |
| 395 | ||
| 396 | // Ensure that styles aren't doubled-up. | |
| 397 | editor.clearStyle( paraId ); | |
| 398 | ||
| 399 | spellcheck( editor, text, paraId ); | |
| 400 | } ); | |
| 401 | } | |
| 402 | ||
| 403 | /** | |
| 404 | * Listen for new tab selection events. | |
| 405 | */ | |
| 406 | private void initTabChangedListener() { | |
| 407 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 408 | ||
| 409 | // Update the preview pane changing tabs. | |
| 410 | editorPane.addTabSelectionListener( | |
| 411 | ( tabPane, oldTab, newTab ) -> { | |
| 412 | if( newTab == null ) { | |
| 413 | // Clear the preview pane when closing an editor. When the last | |
| 414 | // tab is closed, this ensures that the preview pane is empty. | |
| 415 | getPreviewPane().clear(); | |
| 416 | } | |
| 417 | else { | |
| 418 | final var tab = (FileEditorTab) newTab; | |
| 419 | updateVariableNameInjector( tab ); | |
| 420 | process( tab ); | |
| 421 | } | |
| 422 | } | |
| 423 | ); | |
| 424 | } | |
| 425 | ||
| 426 | /** | |
| 427 | * Reloads the preferences from the previous session. | |
| 428 | */ | |
| 429 | private void initPreferences() { | |
| 430 | initDefinitionPane(); | |
| 431 | getFileEditorPane().initPreferences(); | |
| 432 | getUserPreferences().addSaveEventHandler( mRPreferencesListener ); | |
| 433 | } | |
| 434 | ||
| 435 | private void initVariableNameInjector() { | |
| 436 | updateVariableNameInjector( getActiveFileEditorTab() ); | |
| 437 | } | |
| 438 | ||
| 439 | /** | |
| 440 | * Calls the listener when the given node is shown for the first time. The | |
| 441 | * visible property is not the same as the initial showing event; visibility | |
| 442 | * can be triggered numerous times (such as going off screen). | |
| 443 | * <p> | |
| 444 | * This is called, for example, before the drag handler can be attached, | |
| 445 | * because the scrollbar for the text editor pane must be visible. | |
| 446 | * </p> | |
| 447 | * | |
| 448 | * @param node The node to watch for showing. | |
| 449 | * @param consumer The consumer to invoke when the event fires. | |
| 450 | */ | |
| 451 | private void addShowListener( | |
| 452 | final Node node, final Consumer<Void> consumer ) { | |
| 453 | final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) -> | |
| 454 | runLater( () -> { | |
| 455 | if( newShow != null && newShow ) { | |
| 456 | try { | |
| 457 | consumer.accept( null ); | |
| 458 | } catch( final Exception ex ) { | |
| 459 | alert( ex ); | |
| 460 | } | |
| 461 | } | |
| 462 | } ); | |
| 463 | ||
| 464 | Val.flatMap( node.sceneProperty(), Scene::windowProperty ) | |
| 465 | .flatMap( Window::showingProperty ) | |
| 466 | .addListener( listener ); | |
| 467 | } | |
| 468 | ||
| 469 | private void scrollToParagraph( final int id ) { | |
| 470 | scrollToParagraph( id, false ); | |
| 471 | } | |
| 472 | ||
| 473 | /** | |
| 474 | * @param id The paragraph to scroll to, will be approximated if it doesn't | |
| 475 | * exist. | |
| 476 | * @param force {@code true} means to force scrolling immediately, which | |
| 477 | * should only be attempted when it is known that the document | |
| 478 | * has been fully rendered. Otherwise the internal map of ID | |
| 479 | * attributes will be incomplete and scrolling will flounder. | |
| 480 | */ | |
| 481 | private void scrollToParagraph( final int id, final boolean force ) { | |
| 482 | synchronized( mMutex ) { | |
| 483 | final var previewPane = getPreviewPane(); | |
| 484 | final var scrollPane = previewPane.getScrollPane(); | |
| 485 | final int approxId = getActiveEditorPane().approximateParagraphId( id ); | |
| 486 | ||
| 487 | if( force ) { | |
| 488 | previewPane.scrollTo( approxId ); | |
| 489 | } | |
| 490 | else { | |
| 491 | previewPane.tryScrollTo( approxId ); | |
| 492 | } | |
| 493 | ||
| 494 | scrollPane.repaint(); | |
| 495 | } | |
| 496 | } | |
| 497 | ||
| 498 | private void updateVariableNameInjector( final FileEditorTab tab ) { | |
| 499 | getDefinitionNameInjector().addListener( tab ); | |
| 500 | } | |
| 501 | ||
| 502 | /** | |
| 503 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 504 | * tab. This can be called when the text changes, the caret paragraph | |
| 505 | * changes, or the file tab changes. | |
| 506 | * | |
| 507 | * @param tab The file editor tab that has been changed in some fashion. | |
| 508 | */ | |
| 509 | private void process( final FileEditorTab tab ) { | |
| 510 | if( tab != null ) { | |
| 511 | getPreviewPane().setPath( tab.getPath() ); | |
| 512 | ||
| 513 | final Processor<String> processor = getProcessors().computeIfAbsent( | |
| 514 | tab, p -> createProcessors( tab ) | |
| 515 | ); | |
| 516 | ||
| 517 | try { | |
| 518 | processChain( processor, tab.getEditorText() ); | |
| 519 | } catch( final Exception ex ) { | |
| 520 | alert( ex ); | |
| 521 | } | |
| 522 | } | |
| 523 | } | |
| 524 | ||
| 525 | /** | |
| 526 | * Executes the processing chain, operating on the given string. | |
| 527 | * | |
| 528 | * @param handler The first processor in the chain to call. | |
| 529 | * @param text The initial value of the text to process. | |
| 530 | * @return The final value of the text that was processed by the chain. | |
| 531 | */ | |
| 532 | private String processChain( Processor<String> handler, String text ) { | |
| 533 | while( handler != null && text != null ) { | |
| 534 | text = handler.apply( text ); | |
| 535 | handler = handler.next(); | |
| 536 | } | |
| 537 | ||
| 538 | return text; | |
| 539 | } | |
| 540 | ||
| 541 | private void renderActiveTab() { | |
| 542 | process( getActiveFileEditorTab() ); | |
| 543 | } | |
| 544 | ||
| 545 | /** | |
| 546 | * Called when a definition source is opened. | |
| 547 | * | |
| 548 | * @param path Path to the definition source that was opened. | |
| 549 | */ | |
| 550 | private void openDefinitions( final Path path ) { | |
| 551 | try { | |
| 552 | final var ds = createDefinitionSource( path ); | |
| 553 | setDefinitionSource( ds ); | |
| 554 | ||
| 555 | final var prefs = getUserPreferences(); | |
| 556 | prefs.definitionPathProperty().setValue( path.toFile() ); | |
| 557 | prefs.save(); | |
| 558 | ||
| 559 | final var tooltipPath = new Tooltip( path.toString() ); | |
| 560 | tooltipPath.setShowDelay( Duration.millis( 200 ) ); | |
| 561 | ||
| 562 | final var pane = getDefinitionPane(); | |
| 563 | pane.update( ds ); | |
| 564 | pane.addTreeChangeHandler( mTreeHandler ); | |
| 565 | pane.addKeyEventHandler( mDefinitionKeyHandler ); | |
| 566 | pane.filenameProperty().setValue( path.getFileName().toString() ); | |
| 567 | pane.setTooltip( tooltipPath ); | |
| 568 | ||
| 569 | interpolateResolvedMap(); | |
| 570 | } catch( final Exception ex ) { | |
| 571 | alert( ex ); | |
| 572 | } | |
| 573 | } | |
| 574 | ||
| 575 | private void exportDefinitions( final Path path ) { | |
| 576 | try { | |
| 577 | final var pane = getDefinitionPane(); | |
| 578 | final var root = pane.getTreeView().getRoot(); | |
| 579 | final var problemChild = pane.isTreeWellFormed(); | |
| 580 | ||
| 581 | if( problemChild == null ) { | |
| 582 | getDefinitionSource().getTreeAdapter().export( root, path ); | |
| 583 | } | |
| 584 | else { | |
| 585 | alert( "yaml.error.tree.form", problemChild.getValue() ); | |
| 586 | } | |
| 587 | } catch( final Exception ex ) { | |
| 588 | alert( ex ); | |
| 589 | } | |
| 590 | } | |
| 591 | ||
| 592 | private void interpolateResolvedMap() { | |
| 593 | final var treeMap = getDefinitionPane().toMap(); | |
| 594 | final var map = new HashMap<>( treeMap ); | |
| 595 | MapInterpolator.interpolate( map ); | |
| 596 | ||
| 597 | getResolvedMap().clear(); | |
| 598 | getResolvedMap().putAll( map ); | |
| 599 | } | |
| 600 | ||
| 601 | private void initDefinitionPane() { | |
| 602 | openDefinitions( getDefinitionPath() ); | |
| 603 | } | |
| 604 | ||
| 605 | //---- File actions ------------------------------------------------------- | |
| 606 | ||
| 607 | /** | |
| 608 | * Called when an {@link Observable} instance has changed. This is called | |
| 609 | * by both the {@link Snitch} service and the notify service. The @link | |
| 610 | * Snitch} service can be called for different file types, including | |
| 611 | * {@link DefinitionSource} instances. | |
| 612 | * | |
| 613 | * @param observable The observed instance. | |
| 614 | * @param value The noteworthy item. | |
| 615 | */ | |
| 616 | @Override | |
| 617 | public void update( final Observable observable, final Object value ) { | |
| 618 | if( value instanceof Path && observable instanceof Snitch ) { | |
| 619 | updateSelectedTab(); | |
| 620 | } | |
| 621 | } | |
| 622 | ||
| 623 | /** | |
| 624 | * Called when a file has been modified. | |
| 625 | */ | |
| 626 | private void updateSelectedTab() { | |
| 627 | rerender(); | |
| 628 | } | |
| 629 | ||
| 630 | /** | |
| 631 | * After resetting the processors, they will refresh anew to be up-to-date | |
| 632 | * with the files (text and definition) currently loaded into the editor. | |
| 633 | */ | |
| 634 | private void resetProcessors() { | |
| 635 | getProcessors().clear(); | |
| 636 | } | |
| 637 | ||
| 638 | //---- File actions ------------------------------------------------------- | |
| 639 | ||
| 640 | private void fileNew() { | |
| 641 | getFileEditorPane().newEditor(); | |
| 642 | } | |
| 643 | ||
| 644 | private void fileOpen() { | |
| 645 | getFileEditorPane().openFileDialog(); | |
| 646 | } | |
| 647 | ||
| 648 | private void fileClose() { | |
| 649 | getFileEditorPane().closeEditor( getActiveFileEditorTab(), true ); | |
| 650 | } | |
| 651 | ||
| 652 | /** | |
| 653 | * TODO: Upon closing, first remove the tab change listeners. (There's no | |
| 654 | * need to re-render each tab when all are being closed.) | |
| 655 | */ | |
| 656 | private void fileCloseAll() { | |
| 657 | getFileEditorPane().closeAllEditors(); | |
| 658 | } | |
| 659 | ||
| 660 | private void fileSave() { | |
| 661 | getFileEditorPane().saveEditor( getActiveFileEditorTab() ); | |
| 662 | } | |
| 663 | ||
| 664 | private void fileSaveAs() { | |
| 665 | final FileEditorTab editor = getActiveFileEditorTab(); | |
| 666 | getFileEditorPane().saveEditorAs( editor ); | |
| 667 | getProcessors().remove( editor ); | |
| 668 | ||
| 669 | try { | |
| 670 | process( editor ); | |
| 671 | } catch( final Exception ex ) { | |
| 672 | alert( ex ); | |
| 673 | } | |
| 674 | } | |
| 675 | ||
| 676 | private void fileSaveAll() { | |
| 677 | getFileEditorPane().saveAllEditors(); | |
| 678 | } | |
| 679 | ||
| 680 | private void fileExit() { | |
| 681 | final Window window = getWindow(); | |
| 682 | fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | |
| 683 | } | |
| 684 | ||
| 685 | //---- Edit actions ------------------------------------------------------- | |
| 686 | ||
| 687 | /** | |
| 688 | * Transform the Markdown into HTML then copy that HTML into the copy | |
| 689 | * buffer. | |
| 690 | */ | |
| 691 | private void copyHtml() { | |
| 692 | final var markdown = getActiveEditorPane().getText(); | |
| 693 | final var processors = createProcessorFactory().createProcessors( | |
| 694 | getActiveFileEditorTab() | |
| 695 | ); | |
| 696 | ||
| 697 | final var chain = processors.remove( HtmlPreviewProcessor.class ); | |
| 698 | ||
| 699 | final String html = processChain( chain, markdown ); | |
| 700 | ||
| 701 | final Clipboard clipboard = Clipboard.getSystemClipboard(); | |
| 702 | final ClipboardContent content = new ClipboardContent(); | |
| 703 | content.putString( html ); | |
| 704 | clipboard.setContent( content ); | |
| 705 | } | |
| 706 | ||
| 707 | /** | |
| 708 | * Used to find text in the active file editor window. | |
| 709 | */ | |
| 710 | private void editFind() { | |
| 711 | final TextField input = getFindTextField(); | |
| 712 | getStatusBar().setGraphic( input ); | |
| 713 | input.requestFocus(); | |
| 714 | } | |
| 715 | ||
| 716 | public void editFindNext() { | |
| 717 | getActiveFileEditorTab().searchNext( getFindTextField().getText() ); | |
| 718 | } | |
| 719 | ||
| 720 | public void editPreferences() { | |
| 721 | getUserPreferences().show(); | |
| 722 | } | |
| 723 | ||
| 724 | //---- Insert actions ----------------------------------------------------- | |
| 725 | ||
| 726 | /** | |
| 727 | * Delegates to the active editor to handle wrapping the current text | |
| 728 | * selection with leading and trailing strings. | |
| 729 | * | |
| 730 | * @param leading The string to put before the selection. | |
| 731 | * @param trailing The string to put after the selection. | |
| 732 | */ | |
| 733 | private void insertMarkdown( | |
| 734 | final String leading, final String trailing ) { | |
| 735 | getActiveEditorPane().surroundSelection( leading, trailing ); | |
| 736 | } | |
| 737 | ||
| 738 | private void insertMarkdown( | |
| 739 | final String leading, final String trailing, final String hint ) { | |
| 740 | getActiveEditorPane().surroundSelection( leading, trailing, hint ); | |
| 741 | } | |
| 742 | ||
| 743 | //---- View actions ------------------------------------------------------- | |
| 744 | ||
| 745 | private void viewRefresh() { | |
| 746 | rerender(); | |
| 747 | } | |
| 748 | ||
| 749 | //---- Help actions ------------------------------------------------------- | |
| 750 | ||
| 751 | private void helpAbout() { | |
| 752 | final Alert alert = new Alert( INFORMATION ); | |
| 753 | alert.setTitle( get( "Dialog.about.title", APP_TITLE ) ); | |
| 754 | alert.setHeaderText( get( "Dialog.about.header", APP_TITLE ) ); | |
| 755 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 756 | alert.setGraphic( new ImageView( ICON_DIALOG ) ); | |
| 757 | alert.initOwner( getWindow() ); | |
| 758 | ||
| 759 | alert.showAndWait(); | |
| 760 | } | |
| 761 | ||
| 762 | //---- Member creators ---------------------------------------------------- | |
| 763 | ||
| 764 | private SpellChecker createSpellChecker() { | |
| 765 | try { | |
| 766 | final Collection<String> lexicon = readLexicon( "en.txt" ); | |
| 767 | return SymSpellSpeller.forLexicon( lexicon ); | |
| 768 | } catch( final Exception ex ) { | |
| 769 | alert( ex ); | |
| 770 | return new PermissiveSpeller(); | |
| 771 | } | |
| 772 | } | |
| 773 | ||
| 774 | /** | |
| 775 | * Factory to create processors that are suited to different file types. | |
| 776 | * | |
| 777 | * @param tab The tab that is subjected to processing. | |
| 778 | * @return A processor suited to the file type specified by the tab's path. | |
| 779 | */ | |
| 780 | private Processor<String> createProcessors( final FileEditorTab tab ) { | |
| 781 | return createProcessorFactory().createProcessors( tab ); | |
| 782 | } | |
| 783 | ||
| 784 | private ProcessorFactory createProcessorFactory() { | |
| 785 | return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); | |
| 786 | } | |
| 787 | ||
| 788 | private DefinitionPane createDefinitionPane() { | |
| 789 | return new DefinitionPane(); | |
| 790 | } | |
| 791 | ||
| 792 | private HTMLPreviewPane createHTMLPreviewPane() { | |
| 793 | return new HTMLPreviewPane(); | |
| 794 | } | |
| 795 | ||
| 796 | private DefinitionSource createDefaultDefinitionSource() { | |
| 797 | return new YamlDefinitionSource( getDefinitionPath() ); | |
| 798 | } | |
| 799 | ||
| 800 | private DefinitionSource createDefinitionSource( final Path path ) { | |
| 801 | try { | |
| 802 | return createDefinitionFactory().createDefinitionSource( path ); | |
| 803 | } catch( final Exception ex ) { | |
| 804 | alert( ex ); | |
| 805 | return createDefaultDefinitionSource(); | |
| 806 | } | |
| 807 | } | |
| 808 | ||
| 809 | private TextField createFindTextField() { | |
| 810 | return new TextField(); | |
| 811 | } | |
| 812 | ||
| 813 | private DefinitionFactory createDefinitionFactory() { | |
| 814 | return new DefinitionFactory(); | |
| 815 | } | |
| 816 | ||
| 817 | private StatusBar createStatusBar() { | |
| 818 | return new StatusBar(); | |
| 819 | } | |
| 820 | ||
| 821 | private Scene createScene() { | |
| 822 | final SplitPane splitPane = new SplitPane( | |
| 823 | getDefinitionPane(), | |
| 824 | getFileEditorPane(), | |
| 825 | getPreviewPane() ); | |
| 826 | ||
| 827 | splitPane.setDividerPositions( | |
| 828 | getFloat( K_PANE_SPLIT_DEFINITION, .22f ), | |
| 829 | getFloat( K_PANE_SPLIT_EDITOR, .60f ), | |
| 830 | getFloat( K_PANE_SPLIT_PREVIEW, .18f ) ); | |
| 831 | ||
| 832 | getDefinitionPane().prefHeightProperty() | |
| 833 | .bind( splitPane.heightProperty() ); | |
| 834 | ||
| 835 | final BorderPane borderPane = new BorderPane(); | |
| 836 | borderPane.setPrefSize( 1280, 800 ); | |
| 837 | borderPane.setTop( createMenuBar() ); | |
| 838 | borderPane.setBottom( getStatusBar() ); | |
| 839 | borderPane.setCenter( splitPane ); | |
| 840 | ||
| 841 | final VBox statusBar = new VBox(); | |
| 842 | statusBar.setAlignment( Pos.BASELINE_CENTER ); | |
| 843 | statusBar.getChildren().add( getLineNumberText() ); | |
| 844 | getStatusBar().getRightItems().add( statusBar ); | |
| 845 | ||
| 846 | // Force preview pane refresh on Windows. | |
| 847 | if( SystemUtils.IS_OS_WINDOWS ) { | |
| 848 | splitPane.getDividers().get( 1 ).positionProperty().addListener( | |
| 849 | ( l, oValue, nValue ) -> runLater( | |
| 850 | () -> getPreviewPane().getScrollPane().repaint() | |
| 851 | ) | |
| 852 | ); | |
| 853 | } | |
| 854 | ||
| 855 | return new Scene( borderPane ); | |
| 856 | } | |
| 857 | ||
| 858 | private Text createLineNumberText() { | |
| 859 | return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) ); | |
| 860 | } | |
| 861 | ||
| 862 | private Node createMenuBar() { | |
| 863 | final BooleanBinding activeFileEditorIsNull = | |
| 864 | getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 865 | ||
| 866 | // File actions | |
| 867 | final Action fileNewAction = new ActionBuilder() | |
| 868 | .setText( "Main.menu.file.new" ) | |
| 869 | .setAccelerator( "Shortcut+N" ) | |
| 870 | .setIcon( FILE_ALT ) | |
| 871 | .setAction( e -> fileNew() ) | |
| 872 | .build(); | |
| 873 | final Action fileOpenAction = new ActionBuilder() | |
| 874 | .setText( "Main.menu.file.open" ) | |
| 875 | .setAccelerator( "Shortcut+O" ) | |
| 876 | .setIcon( FOLDER_OPEN_ALT ) | |
| 877 | .setAction( e -> fileOpen() ) | |
| 878 | .build(); | |
| 879 | final Action fileCloseAction = new ActionBuilder() | |
| 880 | .setText( "Main.menu.file.close" ) | |
| 881 | .setAccelerator( "Shortcut+W" ) | |
| 882 | .setAction( e -> fileClose() ) | |
| 883 | .setDisable( activeFileEditorIsNull ) | |
| 884 | .build(); | |
| 885 | final Action fileCloseAllAction = new ActionBuilder() | |
| 886 | .setText( "Main.menu.file.close_all" ) | |
| 887 | .setAction( e -> fileCloseAll() ) | |
| 888 | .setDisable( activeFileEditorIsNull ) | |
| 889 | .build(); | |
| 890 | final Action fileSaveAction = new ActionBuilder() | |
| 891 | .setText( "Main.menu.file.save" ) | |
| 892 | .setAccelerator( "Shortcut+S" ) | |
| 893 | .setIcon( FLOPPY_ALT ) | |
| 894 | .setAction( e -> fileSave() ) | |
| 895 | .setDisable( createActiveBooleanProperty( | |
| 896 | FileEditorTab::modifiedProperty ).not() ) | |
| 897 | .build(); | |
| 898 | final Action fileSaveAsAction = new ActionBuilder() | |
| 899 | .setText( "Main.menu.file.save_as" ) | |
| 900 | .setAction( e -> fileSaveAs() ) | |
| 901 | .setDisable( activeFileEditorIsNull ) | |
| 902 | .build(); | |
| 903 | final Action fileSaveAllAction = new ActionBuilder() | |
| 904 | .setText( "Main.menu.file.save_all" ) | |
| 905 | .setAccelerator( "Shortcut+Shift+S" ) | |
| 906 | .setAction( e -> fileSaveAll() ) | |
| 907 | .setDisable( Bindings.not( | |
| 908 | getFileEditorPane().anyFileEditorModifiedProperty() ) ) | |
| 909 | .build(); | |
| 910 | final Action fileExitAction = new ActionBuilder() | |
| 911 | .setText( "Main.menu.file.exit" ) | |
| 912 | .setAction( e -> fileExit() ) | |
| 913 | .build(); | |
| 914 | ||
| 915 | // Edit actions | |
| 916 | final Action editCopyHtmlAction = new ActionBuilder() | |
| 917 | .setText( "Main.menu.edit.copy.html" ) | |
| 918 | .setIcon( HTML5 ) | |
| 919 | .setAction( e -> copyHtml() ) | |
| 920 | .setDisable( activeFileEditorIsNull ) | |
| 921 | .build(); | |
| 922 | ||
| 923 | final Action editUndoAction = new ActionBuilder() | |
| 924 | .setText( "Main.menu.edit.undo" ) | |
| 925 | .setAccelerator( "Shortcut+Z" ) | |
| 926 | .setIcon( UNDO ) | |
| 927 | .setAction( e -> getActiveEditorPane().undo() ) | |
| 928 | .setDisable( createActiveBooleanProperty( | |
| 929 | FileEditorTab::canUndoProperty ).not() ) | |
| 930 | .build(); | |
| 931 | final Action editRedoAction = new ActionBuilder() | |
| 932 | .setText( "Main.menu.edit.redo" ) | |
| 933 | .setAccelerator( "Shortcut+Y" ) | |
| 934 | .setIcon( REPEAT ) | |
| 935 | .setAction( e -> getActiveEditorPane().redo() ) | |
| 936 | .setDisable( createActiveBooleanProperty( | |
| 937 | FileEditorTab::canRedoProperty ).not() ) | |
| 938 | .build(); | |
| 939 | ||
| 940 | final Action editCutAction = new ActionBuilder() | |
| 941 | .setText( "Main.menu.edit.cut" ) | |
| 942 | .setAccelerator( "Shortcut+X" ) | |
| 943 | .setIcon( CUT ) | |
| 944 | .setAction( e -> getActiveEditorPane().cut() ) | |
| 945 | .setDisable( activeFileEditorIsNull ) | |
| 946 | .build(); | |
| 947 | final Action editCopyAction = new ActionBuilder() | |
| 948 | .setText( "Main.menu.edit.copy" ) | |
| 949 | .setAccelerator( "Shortcut+C" ) | |
| 950 | .setIcon( COPY ) | |
| 951 | .setAction( e -> getActiveEditorPane().copy() ) | |
| 952 | .setDisable( activeFileEditorIsNull ) | |
| 953 | .build(); | |
| 954 | final Action editPasteAction = new ActionBuilder() | |
| 955 | .setText( "Main.menu.edit.paste" ) | |
| 956 | .setAccelerator( "Shortcut+V" ) | |
| 957 | .setIcon( PASTE ) | |
| 958 | .setAction( e -> getActiveEditorPane().paste() ) | |
| 959 | .setDisable( activeFileEditorIsNull ) | |
| 960 | .build(); | |
| 961 | final Action editSelectAllAction = new ActionBuilder() | |
| 962 | .setText( "Main.menu.edit.selectAll" ) | |
| 963 | .setAccelerator( "Shortcut+A" ) | |
| 964 | .setAction( e -> getActiveEditorPane().selectAll() ) | |
| 965 | .setDisable( activeFileEditorIsNull ) | |
| 966 | .build(); | |
| 967 | ||
| 968 | final Action editFindAction = new ActionBuilder() | |
| 969 | .setText( "Main.menu.edit.find" ) | |
| 970 | .setAccelerator( "Ctrl+F" ) | |
| 971 | .setIcon( SEARCH ) | |
| 972 | .setAction( e -> editFind() ) | |
| 973 | .setDisable( activeFileEditorIsNull ) | |
| 974 | .build(); | |
| 975 | final Action editFindNextAction = new ActionBuilder() | |
| 976 | .setText( "Main.menu.edit.find.next" ) | |
| 977 | .setAccelerator( "F3" ) | |
| 978 | .setIcon( null ) | |
| 979 | .setAction( e -> editFindNext() ) | |
| 980 | .setDisable( activeFileEditorIsNull ) | |
| 981 | .build(); | |
| 982 | final Action editPreferencesAction = new ActionBuilder() | |
| 983 | .setText( "Main.menu.edit.preferences" ) | |
| 984 | .setAccelerator( "Ctrl+Alt+S" ) | |
| 985 | .setAction( e -> editPreferences() ) | |
| 986 | .build(); | |
| 987 | ||
| 988 | // Format actions | |
| 989 | final Action formatBoldAction = new ActionBuilder() | |
| 990 | .setText( "Main.menu.format.bold" ) | |
| 991 | .setAccelerator( "Shortcut+B" ) | |
| 992 | .setIcon( BOLD ) | |
| 993 | .setAction( e -> insertMarkdown( "**", "**" ) ) | |
| 994 | .setDisable( activeFileEditorIsNull ) | |
| 995 | .build(); | |
| 996 | final Action formatItalicAction = new ActionBuilder() | |
| 997 | .setText( "Main.menu.format.italic" ) | |
| 998 | .setAccelerator( "Shortcut+I" ) | |
| 999 | .setIcon( ITALIC ) | |
| 1000 | .setAction( e -> insertMarkdown( "*", "*" ) ) | |
| 1001 | .setDisable( activeFileEditorIsNull ) | |
| 1002 | .build(); | |
| 1003 | final Action formatSuperscriptAction = new ActionBuilder() | |
| 1004 | .setText( "Main.menu.format.superscript" ) | |
| 1005 | .setAccelerator( "Shortcut+[" ) | |
| 1006 | .setIcon( SUPERSCRIPT ) | |
| 1007 | .setAction( e -> insertMarkdown( "^", "^" ) ) | |
| 1008 | .setDisable( activeFileEditorIsNull ) | |
| 1009 | .build(); | |
| 1010 | final Action formatSubscriptAction = new ActionBuilder() | |
| 1011 | .setText( "Main.menu.format.subscript" ) | |
| 1012 | .setAccelerator( "Shortcut+]" ) | |
| 1013 | .setIcon( SUBSCRIPT ) | |
| 1014 | .setAction( e -> insertMarkdown( "~", "~" ) ) | |
| 1015 | .setDisable( activeFileEditorIsNull ) | |
| 1016 | .build(); | |
| 1017 | final Action formatStrikethroughAction = new ActionBuilder() | |
| 1018 | .setText( "Main.menu.format.strikethrough" ) | |
| 1019 | .setAccelerator( "Shortcut+T" ) | |
| 1020 | .setIcon( STRIKETHROUGH ) | |
| 1021 | .setAction( e -> insertMarkdown( "~~", "~~" ) ) | |
| 1022 | .setDisable( activeFileEditorIsNull ) | |
| 1023 | .build(); | |
| 1024 | ||
| 1025 | // Insert actions | |
| 1026 | final Action insertBlockquoteAction = new ActionBuilder() | |
| 1027 | .setText( "Main.menu.insert.blockquote" ) | |
| 1028 | .setAccelerator( "Ctrl+Q" ) | |
| 1029 | .setIcon( QUOTE_LEFT ) | |
| 1030 | .setAction( e -> insertMarkdown( "\n\n> ", "" ) ) | |
| 1031 | .setDisable( activeFileEditorIsNull ) | |
| 1032 | .build(); | |
| 1033 | final Action insertCodeAction = new ActionBuilder() | |
| 1034 | .setText( "Main.menu.insert.code" ) | |
| 1035 | .setAccelerator( "Shortcut+K" ) | |
| 1036 | .setIcon( CODE ) | |
| 1037 | .setAction( e -> insertMarkdown( "`", "`" ) ) | |
| 1038 | .setDisable( activeFileEditorIsNull ) | |
| 1039 | .build(); | |
| 1040 | final Action insertFencedCodeBlockAction = new ActionBuilder() | |
| 1041 | .setText( "Main.menu.insert.fenced_code_block" ) | |
| 1042 | .setAccelerator( "Shortcut+Shift+K" ) | |
| 1043 | .setIcon( FILE_CODE_ALT ) | |
| 1044 | .setAction( e -> insertMarkdown( | |
| 1045 | "\n\n```\n", | |
| 1046 | "\n```\n\n", | |
| 1047 | get( "Main.menu.insert.fenced_code_block.prompt" ) ) ) | |
| 1048 | .setDisable( activeFileEditorIsNull ) | |
| 1049 | .build(); | |
| 1050 | final Action insertLinkAction = new ActionBuilder() | |
| 1051 | .setText( "Main.menu.insert.link" ) | |
| 1052 | .setAccelerator( "Shortcut+L" ) | |
| 1053 | .setIcon( LINK ) | |
| 1054 | .setAction( e -> getActiveEditorPane().insertLink() ) | |
| 1055 | .setDisable( activeFileEditorIsNull ) | |
| 1056 | .build(); | |
| 1057 | final Action insertImageAction = new ActionBuilder() | |
| 1058 | .setText( "Main.menu.insert.image" ) | |
| 1059 | .setAccelerator( "Shortcut+G" ) | |
| 1060 | .setIcon( PICTURE_ALT ) | |
| 1061 | .setAction( e -> getActiveEditorPane().insertImage() ) | |
| 1062 | .setDisable( activeFileEditorIsNull ) | |
| 1063 | .build(); | |
| 1064 | ||
| 1065 | // Number of heading actions (H1 ... H3) | |
| 1066 | final int HEADINGS = 3; | |
| 1067 | final Action[] headings = new Action[ HEADINGS ]; | |
| 1068 | ||
| 1069 | for( int i = 1; i <= HEADINGS; i++ ) { | |
| 1070 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 1071 | final String markup = String.format( "%n%n%s ", hashes ); | |
| 1072 | final String text = "Main.menu.insert.heading." + i; | |
| 1073 | final String accelerator = "Shortcut+" + i; | |
| 1074 | final String prompt = text + ".prompt"; | |
| 1075 | ||
| 1076 | headings[ i - 1 ] = new ActionBuilder() | |
| 1077 | .setText( text ) | |
| 1078 | .setAccelerator( accelerator ) | |
| 1079 | .setIcon( HEADER ) | |
| 1080 | .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) ) | |
| 1081 | .setDisable( activeFileEditorIsNull ) | |
| 1082 | .build(); | |
| 1083 | } | |
| 1084 | ||
| 1085 | final Action insertUnorderedListAction = new ActionBuilder() | |
| 1086 | .setText( "Main.menu.insert.unordered_list" ) | |
| 1087 | .setAccelerator( "Shortcut+U" ) | |
| 1088 | .setIcon( LIST_UL ) | |
| 1089 | .setAction( e -> insertMarkdown( "\n\n* ", "" ) ) | |
| 1090 | .setDisable( activeFileEditorIsNull ) | |
| 1091 | .build(); | |
| 1092 | final Action insertOrderedListAction = new ActionBuilder() | |
| 1093 | .setText( "Main.menu.insert.ordered_list" ) | |
| 1094 | .setAccelerator( "Shortcut+Shift+O" ) | |
| 1095 | .setIcon( LIST_OL ) | |
| 1096 | .setAction( e -> insertMarkdown( | |
| 1097 | "\n\n1. ", "" ) ) | |
| 1098 | .setDisable( activeFileEditorIsNull ) | |
| 1099 | .build(); | |
| 1100 | final Action insertHorizontalRuleAction = new ActionBuilder() | |
| 1101 | .setText( "Main.menu.insert.horizontal_rule" ) | |
| 1102 | .setAccelerator( "Shortcut+H" ) | |
| 1103 | .setAction( e -> insertMarkdown( | |
| 1104 | "\n\n---\n\n", "" ) ) | |
| 1105 | .setDisable( activeFileEditorIsNull ) | |
| 1106 | .build(); | |
| 1107 | ||
| 1108 | // Definition actions | |
| 1109 | final Action definitionCreateAction = new ActionBuilder() | |
| 1110 | .setText( "Main.menu.definition.create" ) | |
| 1111 | .setIcon( TREE ) | |
| 1112 | .setAction( e -> getDefinitionPane().addItem() ) | |
| 1113 | .build(); | |
| 1114 | final Action definitionInsertAction = new ActionBuilder() | |
| 1115 | .setText( "Main.menu.definition.insert" ) | |
| 1116 | .setAccelerator( "Ctrl+Space" ) | |
| 1117 | .setIcon( STAR ) | |
| 1118 | .setAction( e -> definitionInsert() ) | |
| 1119 | .build(); | |
| 1120 | ||
| 1121 | // Help actions | |
| 1122 | final Action helpAboutAction = new ActionBuilder() | |
| 1123 | .setText( "Main.menu.help.about" ) | |
| 1124 | .setAction( e -> helpAbout() ) | |
| 1125 | .build(); | |
| 1126 | ||
| 1127 | //---- MenuBar ---- | |
| 1128 | ||
| 1129 | // File Menu | |
| 1130 | final var fileMenu = ActionUtils.createMenu( | |
| 1131 | get( "Main.menu.file" ), | |
| 1132 | fileNewAction, | |
| 1133 | fileOpenAction, | |
| 1134 | null, | |
| 1135 | fileCloseAction, | |
| 1136 | fileCloseAllAction, | |
| 1137 | null, | |
| 1138 | fileSaveAction, | |
| 1139 | fileSaveAsAction, | |
| 1140 | fileSaveAllAction, | |
| 1141 | null, | |
| 1142 | fileExitAction ); | |
| 1143 | ||
| 1144 | // Edit Menu | |
| 1145 | final var editMenu = ActionUtils.createMenu( | |
| 1146 | get( "Main.menu.edit" ), | |
| 1147 | editCopyHtmlAction, | |
| 1148 | null, | |
| 1149 | editUndoAction, | |
| 1150 | editRedoAction, | |
| 1151 | null, | |
| 1152 | editCutAction, | |
| 1153 | editCopyAction, | |
| 1154 | editPasteAction, | |
| 1155 | editSelectAllAction, | |
| 1156 | null, | |
| 1157 | editFindAction, | |
| 1158 | editFindNextAction, | |
| 1159 | null, | |
| 1160 | editPreferencesAction ); | |
| 1161 | ||
| 1162 | // Format Menu | |
| 1163 | final var formatMenu = ActionUtils.createMenu( | |
| 1164 | get( "Main.menu.format" ), | |
| 1165 | formatBoldAction, | |
| 1166 | formatItalicAction, | |
| 1167 | formatSuperscriptAction, | |
| 1168 | formatSubscriptAction, | |
| 1169 | formatStrikethroughAction | |
| 1170 | ); | |
| 1171 | ||
| 1172 | // Insert Menu | |
| 1173 | final var insertMenu = ActionUtils.createMenu( | |
| 1174 | get( "Main.menu.insert" ), | |
| 1175 | insertBlockquoteAction, | |
| 1176 | insertCodeAction, | |
| 1177 | insertFencedCodeBlockAction, | |
| 1178 | null, | |
| 1179 | insertLinkAction, | |
| 1180 | insertImageAction, | |
| 1181 | null, | |
| 1182 | headings[ 0 ], | |
| 1183 | headings[ 1 ], | |
| 1184 | headings[ 2 ], | |
| 1185 | null, | |
| 1186 | insertUnorderedListAction, | |
| 1187 | insertOrderedListAction, | |
| 1188 | insertHorizontalRuleAction | |
| 1189 | ); | |
| 1190 | ||
| 1191 | // Definition Menu | |
| 1192 | final var definitionMenu = ActionUtils.createMenu( | |
| 1193 | get( "Main.menu.definition" ), | |
| 1194 | definitionCreateAction, | |
| 1195 | definitionInsertAction ); | |
| 1196 | ||
| 1197 | // Help Menu | |
| 1198 | final var helpMenu = ActionUtils.createMenu( | |
| 1199 | get( "Main.menu.help" ), | |
| 1200 | helpAboutAction ); | |
| 1201 | ||
| 1202 | //---- MenuBar ---- | |
| 1203 | final var menuBar = new MenuBar( | |
| 1204 | fileMenu, | |
| 1205 | editMenu, | |
| 1206 | formatMenu, | |
| 1207 | insertMenu, | |
| 1208 | definitionMenu, | |
| 1209 | helpMenu ); | |
| 1210 | ||
| 1211 | //---- ToolBar ---- | |
| 1212 | final var toolBar = ActionUtils.createToolBar( | |
| 1213 | fileNewAction, | |
| 1214 | fileOpenAction, | |
| 1215 | fileSaveAction, | |
| 1216 | null, | |
| 1217 | editUndoAction, | |
| 1218 | editRedoAction, | |
| 1219 | editCutAction, | |
| 1220 | editCopyAction, | |
| 1221 | editPasteAction, | |
| 1222 | null, | |
| 1223 | formatBoldAction, | |
| 1224 | formatItalicAction, | |
| 1225 | formatSuperscriptAction, | |
| 1226 | formatSubscriptAction, | |
| 1227 | insertBlockquoteAction, | |
| 1228 | insertCodeAction, | |
| 1229 | insertFencedCodeBlockAction, | |
| 1230 | null, | |
| 1231 | insertLinkAction, | |
| 1232 | insertImageAction, | |
| 1233 | null, | |
| 1234 | headings[ 0 ], | |
| 1235 | null, | |
| 1236 | insertUnorderedListAction, | |
| 1237 | insertOrderedListAction ); | |
| 1238 | ||
| 1239 | return new VBox( menuBar, toolBar ); | |
| 1240 | } | |
| 1241 | ||
| 1242 | /** | |
| 1243 | * Performs the autoinsert function on the active file editor. | |
| 1244 | */ | |
| 1245 | private void definitionInsert() { | |
| 1246 | getDefinitionNameInjector().autoinsert(); | |
| 1247 | } | |
| 1248 | ||
| 1249 | /** | |
| 1250 | * Creates a boolean property that is bound to another boolean value of the | |
| 1251 | * active editor. | |
| 1252 | */ | |
| 1253 | private BooleanProperty createActiveBooleanProperty( | |
| 1254 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 1255 | ||
| 1256 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 1257 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 1258 | ||
| 1259 | if( tab != null ) { | |
| 1260 | b.bind( func.apply( tab ) ); | |
| 1261 | } | |
| 1262 | ||
| 1263 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 1264 | ( observable, oldFileEditor, newFileEditor ) -> { | |
| 1265 | b.unbind(); | |
| 1266 | ||
| 1267 | if( newFileEditor == null ) { | |
| 1268 | b.set( false ); | |
| 1269 | } | |
| 1270 | else { | |
| 1271 | b.bind( func.apply( newFileEditor ) ); | |
| 1272 | } | |
| 1273 | } | |
| 1274 | ); | |
| 1275 | ||
| 1276 | return b; | |
| 1277 | } | |
| 1278 | ||
| 1279 | //---- Convenience accessors ---------------------------------------------- | |
| 1280 | ||
| 1281 | private Preferences getPreferences() { | |
| 1282 | return sOptions.getState(); | |
| 1283 | } | |
| 1284 | ||
| 1285 | private int getCurrentParagraphIndex() { | |
| 1286 | return getActiveEditorPane().getCurrentParagraphIndex(); | |
| 1287 | } | |
| 1288 | ||
| 1289 | private float getFloat( final String key, final float defaultValue ) { | |
| 1290 | return getPreferences().getFloat( key, defaultValue ); | |
| 1291 | } | |
| 1292 | ||
| 1293 | public Window getWindow() { | |
| 1294 | return getScene().getWindow(); | |
| 1295 | } | |
| 1296 | ||
| 1297 | private MarkdownEditorPane getActiveEditorPane() { | |
| 1298 | return getActiveFileEditorTab().getEditorPane(); | |
| 1299 | } | |
| 1300 | ||
| 1301 | private FileEditorTab getActiveFileEditorTab() { | |
| 1302 | return getFileEditorPane().getActiveFileEditor(); | |
| 1303 | } | |
| 1304 | ||
| 1305 | //---- Member accessors --------------------------------------------------- | |
| 1306 | ||
| 1307 | protected Scene getScene() { | |
| 1308 | return mScene; | |
| 1309 | } | |
| 1310 | ||
| 1311 | private SpellChecker getSpellChecker() { | |
| 1312 | return mSpellChecker; | |
| 1313 | } | |
| 1314 | ||
| 1315 | private Map<FileEditorTab, Processor<String>> getProcessors() { | |
| 1316 | return mProcessors; | |
| 1317 | } | |
| 1318 | ||
| 1319 | private FileEditorTabPane getFileEditorPane() { | |
| 1320 | return mFileEditorPane; | |
| 1321 | } | |
| 1322 | ||
| 1323 | private HTMLPreviewPane getPreviewPane() { | |
| 1324 | return mPreviewPane; | |
| 1325 | } | |
| 1326 | ||
| 1327 | private void setDefinitionSource( | |
| 1328 | final DefinitionSource definitionSource ) { | |
| 1329 | assert definitionSource != null; | |
| 1330 | mDefinitionSource = definitionSource; | |
| 1331 | } | |
| 1332 | ||
| 1333 | private DefinitionSource getDefinitionSource() { | |
| 1334 | return mDefinitionSource; | |
| 1335 | } | |
| 1336 | ||
| 1337 | private DefinitionPane getDefinitionPane() { | |
| 1338 | return mDefinitionPane; | |
| 1339 | } | |
| 1340 | ||
| 1341 | private Text getLineNumberText() { | |
| 1342 | return mLineNumberText; | |
| 1343 | } | |
| 1344 | ||
| 1345 | private StatusBar getStatusBar() { | |
| 1346 | return mStatusBar; | |
| 1347 | } | |
| 1348 | ||
| 1349 | private TextField getFindTextField() { | |
| 1350 | return mFindTextField; | |
| 1351 | } | |
| 1352 | ||
| 1353 | private DefinitionNameInjector getDefinitionNameInjector() { | |
| 1354 | return mDefinitionNameInjector; | |
| 1355 | } | |
| 1356 | ||
| 1357 | /** | |
| 1358 | * Returns the variable map of interpolated definitions. | |
| 1359 | * | |
| 1360 | * @return A map to help dereference variables. | |
| 1361 | */ | |
| 1362 | private Map<String, String> getResolvedMap() { | |
| 1363 | return mResolvedMap; | |
| 1364 | } | |
| 1365 | ||
| 1366 | //---- Persistence accessors ---------------------------------------------- | |
| 1367 | ||
| 1368 | private UserPreferences getUserPreferences() { | |
| 1369 | return UserPreferences.getInstance(); | |
| 1370 | } | |
| 1371 | ||
| 1372 | private Path getDefinitionPath() { | |
| 1373 | return getUserPreferences().getDefinitionPath(); | |
| 1374 | } | |
| 1375 | ||
| 1376 | //---- Spelling ----------------------------------------------------------- | |
| 1377 | ||
| 1378 | /** | |
| 1379 | * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}. | |
| 1380 | * This is called to spell check the document, rather than a single paragraph. | |
| 1381 | * | |
| 1382 | * @param text The full document text. | |
| 1383 | */ | |
| 1384 | private void spellcheck( | |
| 1385 | final StyleClassedTextArea editor, final String text ) { | |
| 1386 | spellcheck( editor, text, -1 ); | |
| 1387 | } | |
| 1388 | ||
| 1389 | /** | |
| 1390 | * Spellchecks a subset of the entire document. | |
| 1391 | * | |
| 1392 | * @param text Look up words for this text in the lexicon. | |
| 1393 | * @param paraId Set to -1 to apply resulting style spans to the entire | |
| 1394 | * text. | |
| 1395 | */ | |
| 1396 | private void spellcheck( | |
| 1397 | final StyleClassedTextArea editor, final String text, final int paraId ) { | |
| 1398 | final var builder = new StyleSpansBuilder<Collection<String>>(); | |
| 1399 | final var runningIndex = new AtomicInteger( 0 ); | |
| 1400 | final var checker = getSpellChecker(); | |
| 1401 | ||
| 1402 | // The text nodes must be relayed through a contextual "visitor" that | |
| 1403 | // can return text in chunks with correlative offsets into the string. | |
| 1404 | // This allows Markdown, R Markdown, XML, and R XML documents to return | |
| 1405 | // sets of words to check. | |
| 1406 | ||
| 1407 | final var node = mParser.parse( text ); | |
| 1408 | final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> { | |
| 1409 | // Treat hyphenated compound words as individual words. | |
| 1410 | final var check = visited.replace( '-', ' ' ); | |
| 1411 | ||
| 1412 | checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> { | |
| 1413 | prevIndex += bIndex; | |
| 1414 | currIndex += bIndex; | |
| 1415 | ||
| 1416 | // Clear styling between lexiconically absent words. | |
| 1417 | builder.add( emptyList(), prevIndex - runningIndex.get() ); | |
| 1418 | builder.add( singleton( "spelling" ), currIndex - prevIndex ); | |
| 1419 | runningIndex.set( currIndex ); | |
| 1420 | } ); | |
| 1421 | } ); | |
| 1422 | ||
| 1423 | visitor.visit( node ); | |
| 1424 | ||
| 1425 | // If the running index was set, at least one word triggered the listener. | |
| 1426 | if( runningIndex.get() > 0 ) { | |
| 1427 | // Clear styling after the last lexiconically absent word. | |
| 1428 | builder.add( emptyList(), text.length() - runningIndex.get() ); | |
| 1429 | ||
| 1430 | final var spans = builder.create(); | |
| 1431 | ||
| 1432 | if( paraId >= 0 ) { | |
| 1433 | editor.setStyleSpans( paraId, 0, spans ); | |
| 1434 | } | |
| 1435 | else { | |
| 1436 | editor.setStyleSpans( 0, spans ); | |
| 1437 | } | |
| 1438 | } | |
| 1439 | } | |
| 1440 | ||
| 1441 | @SuppressWarnings("SameParameterValue") | |
| 1442 | private Collection<String> readLexicon( final String filename ) | |
| 1443 | throws Exception { | |
| 1444 | final var path = "/" + LEXICONS_DIRECTORY + "/" + filename; | |
| 1445 | ||
| 1446 | try( final var resource = getClass().getResourceAsStream( path ) ) { | |
| 1447 | if( resource == null ) { | |
| 1448 | throw new MissingFileException( path ); | |
| 1449 | } | |
| 1450 | ||
| 1451 | try( final var isr = new InputStreamReader( resource, UTF_8 ); | |
| 1452 | final var reader = new BufferedReader( isr ) ) { | |
| 1453 | return reader.lines().collect( Collectors.toList() ); | |
| 1454 | } | |
| 1455 | } | |
| 1456 | } | |
| 1457 | ||
| 1458 | // TODO: #59 -- Replace using Markdown processor instantiated for Markdown | |
| 1459 | // files. | |
| 1460 | private final Parser mParser = Parser.builder().build(); | |
| 1461 | ||
| 1462 | // TODO: #59 -- Replace with generic interface; provide Markdown/XML | |
| 1463 | // implementations. | |
| 1464 | private static final class TextVisitor { | |
| 1465 | private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>( | |
| 1466 | com.vladsch.flexmark.ast.Text.class, this::visit ) | |
| 1467 | ); | |
| 1468 | ||
| 1469 | private final SpellCheckListener mConsumer; | |
| 1470 | ||
| 1471 | public TextVisitor( final SpellCheckListener consumer ) { | |
| 1472 | mConsumer = consumer; | |
| 1473 | } | |
| 1474 | ||
| 1475 | private void visit( final com.vladsch.flexmark.util.ast.Node node ) { | |
| 1476 | if( node instanceof com.vladsch.flexmark.ast.Text ) { | |
| 1477 | mConsumer.accept( node.getChars().toString(), | |
| 1478 | node.getStartOffset(), | |
| 1479 | node.getEndOffset() ); | |
| 1480 | } | |
| 1481 | ||
| 1482 | mVisitor.visitChildren( node ); | |
| 1483 | } | |
| 1484 | } | |
| 1485 | } | |
| 1 | 1486 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * * Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * * Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite; | |
| 28 | ||
| 29 | import java.text.MessageFormat; | |
| 30 | import java.util.ResourceBundle; | |
| 31 | import java.util.Stack; | |
| 32 | ||
| 33 | import static com.keenwrite.Constants.APP_BUNDLE_NAME; | |
| 34 | import static java.util.ResourceBundle.getBundle; | |
| 35 | ||
| 36 | /** | |
| 37 | * Recursively resolves message properties. Property values can refer to other | |
| 38 | * properties using a <code>${var}</code> syntax. | |
| 39 | */ | |
| 40 | public class Messages { | |
| 41 | ||
| 42 | private static final ResourceBundle RESOURCE_BUNDLE = | |
| 43 | getBundle( APP_BUNDLE_NAME ); | |
| 44 | ||
| 45 | private Messages() { | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Return the value of a resource bundle value after having resolved any | |
| 50 | * references to other bundle variables. | |
| 51 | * | |
| 52 | * @param props The bundle containing resolvable properties. | |
| 53 | * @param s The value for a key to resolve. | |
| 54 | * @return The value of the key with all references recursively dereferenced. | |
| 55 | */ | |
| 56 | @SuppressWarnings("SameParameterValue") | |
| 57 | private static String resolve( final ResourceBundle props, final String s ) { | |
| 58 | final int len = s.length(); | |
| 59 | final Stack<StringBuilder> stack = new Stack<>(); | |
| 60 | ||
| 61 | StringBuilder sb = new StringBuilder( 256 ); | |
| 62 | boolean open = false; | |
| 63 | ||
| 64 | for( int i = 0; i < len; i++ ) { | |
| 65 | final char c = s.charAt( i ); | |
| 66 | ||
| 67 | switch( c ) { | |
| 68 | case '$': { | |
| 69 | if( i + 1 < len && s.charAt( i + 1 ) == '{' ) { | |
| 70 | stack.push( sb ); | |
| 71 | sb = new StringBuilder( 256 ); | |
| 72 | i++; | |
| 73 | open = true; | |
| 74 | } | |
| 75 | ||
| 76 | break; | |
| 77 | } | |
| 78 | ||
| 79 | case '}': { | |
| 80 | if( open ) { | |
| 81 | open = false; | |
| 82 | final String name = sb.toString(); | |
| 83 | ||
| 84 | sb = stack.pop(); | |
| 85 | sb.append( props.getString( name ) ); | |
| 86 | break; | |
| 87 | } | |
| 88 | } | |
| 89 | ||
| 90 | default: { | |
| 91 | sb.append( c ); | |
| 92 | break; | |
| 93 | } | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | if( open ) { | |
| 98 | throw new IllegalArgumentException( "missing '}'" ); | |
| 99 | } | |
| 100 | ||
| 101 | return sb.toString(); | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Returns the value for a key from the message bundle. | |
| 106 | * | |
| 107 | * @param key Retrieve the value for this key. | |
| 108 | * @return The value for the key. | |
| 109 | */ | |
| 110 | public static String get( final String key ) { | |
| 111 | try { | |
| 112 | return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) ); | |
| 113 | } catch( final Exception ex ) { | |
| 114 | return key; | |
| 115 | } | |
| 116 | } | |
| 117 | ||
| 118 | public static String getLiteral( final String key ) { | |
| 119 | return RESOURCE_BUNDLE.getString( key ); | |
| 120 | } | |
| 121 | ||
| 122 | public static String get( final String key, final boolean interpolate ) { | |
| 123 | return interpolate ? get( key ) : getLiteral( key ); | |
| 124 | } | |
| 125 | ||
| 126 | /** | |
| 127 | * Returns the value for a key from the message bundle with the arguments | |
| 128 | * replacing <code>{#}</code> place holders. | |
| 129 | * | |
| 130 | * @param key Retrieve the value for this key. | |
| 131 | * @param args The values to substitute for place holders. | |
| 132 | * @return The value for the key. | |
| 133 | */ | |
| 134 | public static String get( final String key, final Object... args ) { | |
| 135 | return MessageFormat.format( get( key ), args ); | |
| 136 | } | |
| 137 | } | |
| 1 | 138 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import javafx.beans.property.BooleanProperty; | |
| 31 | import javafx.beans.property.SimpleBooleanProperty; | |
| 32 | import javafx.event.Event; | |
| 33 | import javafx.event.EventHandler; | |
| 34 | import javafx.scene.Node; | |
| 35 | import javafx.scene.control.ScrollBar; | |
| 36 | import javafx.scene.control.skin.ScrollBarSkin; | |
| 37 | import javafx.scene.input.MouseEvent; | |
| 38 | import javafx.scene.input.ScrollEvent; | |
| 39 | import javafx.scene.layout.StackPane; | |
| 40 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 41 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 42 | ||
| 43 | import javax.swing.*; | |
| 44 | ||
| 45 | import static javafx.geometry.Orientation.VERTICAL; | |
| 46 | ||
| 47 | /** | |
| 48 | * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to | |
| 49 | * an instance of {@link JScrollBar}. | |
| 50 | * <p> | |
| 51 | * Called to synchronize the scrolling areas for either scrolling with the | |
| 52 | * mouse or scrolling using the scrollbar's thumb. Both are required to avoid | |
| 53 | * scrolling on the estimatedScrollYProperty that occurs when text events | |
| 54 | * fire. Scrolling performed for text events are handled separately to ensure | |
| 55 | * the preview panel scrolls to the same position in the Markdown editor, | |
| 56 | * taking into account things like images, tables, and other potentially | |
| 57 | * long vertical presentation items. | |
| 58 | * </p> | |
| 59 | */ | |
| 60 | public final class ScrollEventHandler implements EventHandler<Event> { | |
| 61 | ||
| 62 | private final class MouseHandler implements EventHandler<MouseEvent> { | |
| 63 | private final EventHandler<? super MouseEvent> mOldHandler; | |
| 64 | ||
| 65 | /** | |
| 66 | * Constructs a new handler for mouse scrolling events. | |
| 67 | * | |
| 68 | * @param oldHandler Receives the event after scrolling takes place. | |
| 69 | */ | |
| 70 | private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) { | |
| 71 | mOldHandler = oldHandler; | |
| 72 | } | |
| 73 | ||
| 74 | @Override | |
| 75 | public void handle( final MouseEvent event ) { | |
| 76 | ScrollEventHandler.this.handle( event ); | |
| 77 | mOldHandler.handle( event ); | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | private final class ScrollHandler implements EventHandler<ScrollEvent> { | |
| 82 | @Override | |
| 83 | public void handle( final ScrollEvent event ) { | |
| 84 | ScrollEventHandler.this.handle( event ); | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane; | |
| 89 | private final JScrollBar mPreviewScrollBar; | |
| 90 | private final BooleanProperty mEnabled = new SimpleBooleanProperty(); | |
| 91 | ||
| 92 | /** | |
| 93 | * @param editorScrollPane Scroll event source (human movement). | |
| 94 | * @param previewScrollBar Scroll event destination (corresponding movement). | |
| 95 | */ | |
| 96 | public ScrollEventHandler( | |
| 97 | final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane, | |
| 98 | final JScrollBar previewScrollBar ) { | |
| 99 | mEditorScrollPane = editorScrollPane; | |
| 100 | mPreviewScrollBar = previewScrollBar; | |
| 101 | ||
| 102 | mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() ); | |
| 103 | ||
| 104 | final var thumb = getVerticalScrollBarThumb( mEditorScrollPane ); | |
| 105 | thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) ); | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * Gets a property intended to be bound to selected property of the tab being | |
| 110 | * scrolled. This is required because there's only one preview pane but | |
| 111 | * multiple editor panes. Each editor pane maintains its own scroll position. | |
| 112 | * | |
| 113 | * @return A {@link BooleanProperty} representing whether the scroll | |
| 114 | * events for this tab are to be executed. | |
| 115 | */ | |
| 116 | public BooleanProperty enabledProperty() { | |
| 117 | return mEnabled; | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm | |
| 122 | * is based on Karl Tauber's ratio calculation. | |
| 123 | * | |
| 124 | * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent} | |
| 125 | */ | |
| 126 | @Override | |
| 127 | public void handle( final Event event ) { | |
| 128 | if( isEnabled() ) { | |
| 129 | final var eScrollPane = getEditorScrollPane(); | |
| 130 | final int eScrollY = | |
| 131 | eScrollPane.estimatedScrollYProperty().getValue().intValue(); | |
| 132 | final int eHeight = (int) | |
| 133 | (eScrollPane.totalHeightEstimateProperty().getValue().intValue() | |
| 134 | - eScrollPane.getHeight()); | |
| 135 | final double eRatio = eHeight > 0 | |
| 136 | ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0; | |
| 137 | ||
| 138 | final var pScrollBar = getPreviewScrollBar(); | |
| 139 | final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight(); | |
| 140 | final var pScrollY = (int) (pHeight * eRatio); | |
| 141 | ||
| 142 | pScrollBar.setValue( pScrollY ); | |
| 143 | pScrollBar.getParent().repaint(); | |
| 144 | } | |
| 145 | } | |
| 146 | ||
| 147 | private StackPane getVerticalScrollBarThumb( | |
| 148 | final VirtualizedScrollPane<StyleClassedTextArea> pane ) { | |
| 149 | final ScrollBar scrollBar = getVerticalScrollBar( pane ); | |
| 150 | final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get()); | |
| 151 | ||
| 152 | for( final Node node : skin.getChildren() ) { | |
| 153 | // Brittle, but what can you do? | |
| 154 | if( node.getStyleClass().contains( "thumb" ) ) { | |
| 155 | return (StackPane) node; | |
| 156 | } | |
| 157 | } | |
| 158 | ||
| 159 | throw new IllegalArgumentException( "No scroll bar skin found." ); | |
| 160 | } | |
| 161 | ||
| 162 | private ScrollBar getVerticalScrollBar( | |
| 163 | final VirtualizedScrollPane<StyleClassedTextArea> pane ) { | |
| 164 | ||
| 165 | for( final Node node : pane.getChildrenUnmodifiable() ) { | |
| 166 | if( node instanceof ScrollBar ) { | |
| 167 | final ScrollBar scrollBar = (ScrollBar) node; | |
| 168 | ||
| 169 | if( scrollBar.getOrientation() == VERTICAL ) { | |
| 170 | return scrollBar; | |
| 171 | } | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | throw new IllegalArgumentException( "No vertical scroll pane found." ); | |
| 176 | } | |
| 177 | ||
| 178 | private boolean isEnabled() { | |
| 179 | // TODO: As a minor optimization, when this is set to false, it could remove | |
| 180 | // the MouseHandler and ScrollHandler so that events only dispatch to one | |
| 181 | // object (instead of one per editor tab). | |
| 182 | return mEnabled.get(); | |
| 183 | } | |
| 184 | ||
| 185 | private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() { | |
| 186 | return mEditorScrollPane; | |
| 187 | } | |
| 188 | ||
| 189 | private JScrollBar getPreviewScrollBar() { | |
| 190 | return mPreviewScrollBar; | |
| 191 | } | |
| 192 | } | |
| 1 | 193 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import java.util.HashMap; | |
| 31 | import java.util.Map; | |
| 32 | import java.util.ServiceLoader; | |
| 33 | ||
| 34 | /** | |
| 35 | * Responsible for loading services. The services are treated as singleton | |
| 36 | * instances. | |
| 37 | */ | |
| 38 | public class Services { | |
| 39 | ||
| 40 | @SuppressWarnings("rawtypes") | |
| 41 | private static final Map<Class, Object> SINGLETONS = new HashMap<>(); | |
| 42 | ||
| 43 | /** | |
| 44 | * Loads a service based on its interface definition. This will return an | |
| 45 | * existing instance if the class has already been instantiated. | |
| 46 | * | |
| 47 | * @param <T> The service to load. | |
| 48 | * @param api The interface definition for the service. | |
| 49 | * @return A class that implements the interface. | |
| 50 | */ | |
| 51 | @SuppressWarnings("unchecked") | |
| 52 | public static <T> T load( final Class<T> api ) { | |
| 53 | final T o = (T) get( api ); | |
| 54 | ||
| 55 | return o == null ? newInstance( api ) : o; | |
| 56 | } | |
| 57 | ||
| 58 | private static <T> T newInstance( final Class<T> api ) { | |
| 59 | final ServiceLoader<T> services = ServiceLoader.load( api ); | |
| 60 | ||
| 61 | for( final T service : services ) { | |
| 62 | if( service != null ) { | |
| 63 | // Re-use the same instance the next time the class is loaded. | |
| 64 | put( api, service ); | |
| 65 | return service; | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | throw new RuntimeException( "No implementation for: " + api ); | |
| 70 | } | |
| 71 | ||
| 72 | @SuppressWarnings("rawtypes") | |
| 73 | private static void put( final Class key, Object value ) { | |
| 74 | SINGLETONS.put( key, value ); | |
| 75 | } | |
| 76 | ||
| 77 | @SuppressWarnings("rawtypes") | |
| 78 | private static Object get( final Class api ) { | |
| 79 | return SINGLETONS.get( api ); | |
| 80 | } | |
| 81 | } | |
| 1 | 82 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite; | |
| 29 | ||
| 30 | import com.keenwrite.service.events.Notifier; | |
| 31 | import org.controlsfx.control.StatusBar; | |
| 32 | ||
| 33 | import static com.keenwrite.Constants.STATUS_BAR_OK; | |
| 34 | import static com.keenwrite.Messages.get; | |
| 35 | import static javafx.application.Platform.runLater; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for passing notifications about exceptions (or other error | |
| 39 | * messages) through the application. Once the Event Bus is implemented, this | |
| 40 | * class can go away. | |
| 41 | */ | |
| 42 | public class StatusBarNotifier { | |
| 43 | private static final String OK = get( STATUS_BAR_OK, "OK" ); | |
| 44 | ||
| 45 | private static final Notifier sNotifier = Services.load( Notifier.class ); | |
| 46 | private static StatusBar sStatusBar; | |
| 47 | ||
| 48 | public static void setStatusBar( final StatusBar statusBar ) { | |
| 49 | sStatusBar = statusBar; | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Resets the status bar to a default message. | |
| 54 | */ | |
| 55 | public static void clearAlert() { | |
| 56 | // Don't burden the repaint thread if there's no status bar change. | |
| 57 | if( !OK.equals( sStatusBar.getText() ) ) { | |
| 58 | update( OK ); | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Updates the status bar with a custom message. | |
| 64 | * | |
| 65 | * @param key The resource bundle key associated with a message (typically | |
| 66 | * to inform the user about an error). | |
| 67 | */ | |
| 68 | public static void alert( final String key ) { | |
| 69 | update( get( key ) ); | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Updates the status bar with a custom message. | |
| 74 | * | |
| 75 | * @param key The property key having a value to populate with arguments. | |
| 76 | * @param args The placeholder values to substitute into the key's value. | |
| 77 | */ | |
| 78 | public static void alert( final String key, final Object... args ) { | |
| 79 | update( get( key, args ) ); | |
| 80 | } | |
| 81 | ||
| 82 | /** | |
| 83 | * Called when an exception occurs that warrants the user's attention. | |
| 84 | * | |
| 85 | * @param t The exception with a message that the user should know about. | |
| 86 | */ | |
| 87 | public static void alert( final Throwable t ) { | |
| 88 | update( t.getMessage() ); | |
| 89 | } | |
| 90 | ||
| 91 | /** | |
| 92 | * Updates the status bar to show the first line of the given message. | |
| 93 | * | |
| 94 | * @param message The message to show in the status bar. | |
| 95 | */ | |
| 96 | private static void update( final String message ) { | |
| 97 | runLater( | |
| 98 | () -> { | |
| 99 | final var s = message == null ? "" : message; | |
| 100 | final var i = s.indexOf( '\n' ); | |
| 101 | sStatusBar.setText( s.substring( 0, i > 0 ? i : s.length() ) ); | |
| 102 | } | |
| 103 | ); | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Returns the global {@link Notifier} instance that can be used for opening | |
| 108 | * pop-up alert messages. | |
| 109 | * | |
| 110 | * @return The pop-up {@link Notifier} dispatcher. | |
| 111 | */ | |
| 112 | public static Notifier getNotifier() { | |
| 113 | return sNotifier; | |
| 114 | } | |
| 115 | } | |
| 1 | 116 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.adapters; | |
| 29 | ||
| 30 | import org.xhtmlrenderer.event.DocumentListener; | |
| 31 | ||
| 32 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 33 | ||
| 34 | /** | |
| 35 | * Allows subclasses to implement specific events. | |
| 36 | */ | |
| 37 | public class DocumentAdapter implements DocumentListener { | |
| 38 | @Override | |
| 39 | public void documentStarted() { | |
| 40 | } | |
| 41 | ||
| 42 | @Override | |
| 43 | public void documentLoaded() { | |
| 44 | } | |
| 45 | ||
| 46 | @Override | |
| 47 | public void onLayoutException( final Throwable t ) { | |
| 48 | alert( t ); | |
| 49 | } | |
| 50 | ||
| 51 | @Override | |
| 52 | public void onRenderException( final Throwable t ) { | |
| 53 | alert( t ); | |
| 54 | } | |
| 55 | } | |
| 1 | 56 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.adapters; | |
| 29 | ||
| 30 | import org.w3c.dom.Element; | |
| 31 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 32 | import org.xhtmlrenderer.simple.extend.FormSubmissionListener; | |
| 33 | ||
| 34 | public abstract class ReplacedElementAdapter implements ReplacedElementFactory { | |
| 35 | @Override | |
| 36 | public void reset() { | |
| 37 | } | |
| 38 | ||
| 39 | @Override | |
| 40 | public void remove( final Element e ) { | |
| 41 | } | |
| 42 | ||
| 43 | @Override | |
| 44 | public void setFormSubmissionListener( | |
| 45 | final FormSubmissionListener listener ) { | |
| 46 | } | |
| 47 | } | |
| 1 | 48 |
| 1 | /* | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | package com.keenwrite.controls; | |
| 29 | ||
| 30 | import com.keenwrite.Messages; | |
| 31 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 32 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 33 | import javafx.beans.property.ObjectProperty; | |
| 34 | import javafx.beans.property.SimpleObjectProperty; | |
| 35 | import javafx.event.ActionEvent; | |
| 36 | import javafx.scene.control.Button; | |
| 37 | import javafx.scene.control.Tooltip; | |
| 38 | import javafx.scene.input.KeyCode; | |
| 39 | import javafx.scene.input.KeyEvent; | |
| 40 | import javafx.stage.FileChooser; | |
| 41 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 42 | ||
| 43 | import java.io.File; | |
| 44 | import java.nio.file.Path; | |
| 45 | import java.util.ArrayList; | |
| 46 | import java.util.List; | |
| 47 | ||
| 48 | /** | |
| 49 | * Button that opens a file chooser to select a local file for a URL. | |
| 50 | */ | |
| 51 | public class BrowseFileButton extends Button { | |
| 52 | private final List<ExtensionFilter> extensionFilters = new ArrayList<>(); | |
| 53 | ||
| 54 | public BrowseFileButton() { | |
| 55 | setGraphic( | |
| 56 | FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT ) | |
| 57 | ); | |
| 58 | setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) ); | |
| 59 | setOnAction( this::browse ); | |
| 60 | ||
| 61 | disableProperty().bind( basePath.isNull() ); | |
| 62 | ||
| 63 | // workaround for a JavaFX bug: | |
| 64 | // avoid closing the dialog that contains this control when the user | |
| 65 | // closes the FileChooser or DirectoryChooser using the ESC key | |
| 66 | addEventHandler( KeyEvent.KEY_RELEASED, e -> { | |
| 67 | if( e.getCode() == KeyCode.ESCAPE ) { | |
| 68 | e.consume(); | |
| 69 | } | |
| 70 | } ); | |
| 71 | } | |
| 72 | ||
| 73 | public void addExtensionFilter( ExtensionFilter extensionFilter ) { | |
| 74 | extensionFilters.add( extensionFilter ); | |
| 75 | } | |
| 76 | ||
| 77 | // 'basePath' property | |
| 78 | private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>(); | |
| 79 | ||
| 80 | public Path getBasePath() { | |
| 81 | return basePath.get(); | |
| 82 | } | |
| 83 | ||
| 84 | public void setBasePath( Path basePath ) { | |
| 85 | this.basePath.set( basePath ); | |
| 86 | } | |
| 87 | ||
| 88 | // 'url' property | |
| 89 | private final ObjectProperty<String> url = new SimpleObjectProperty<>(); | |
| 90 | ||
| 91 | public ObjectProperty<String> urlProperty() { | |
| 92 | return url; | |
| 93 | } | |
| 94 | ||
| 95 | protected void browse( ActionEvent e ) { | |
| 96 | FileChooser fileChooser = new FileChooser(); | |
| 97 | fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) ); | |
| 98 | fileChooser.getExtensionFilters().addAll( extensionFilters ); | |
| 99 | fileChooser.getExtensionFilters() | |
| 100 | .add( new ExtensionFilter( Messages.get( | |
| 101 | "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) ); | |
| 102 | fileChooser.setInitialDirectory( getInitialDirectory() ); | |
| 103 | File result = fileChooser.showOpenDialog( getScene().getWindow() ); | |
| 104 | if( result != null ) { | |
| 105 | updateUrl( result ); | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | protected File getInitialDirectory() { | |
| 110 | //TODO build initial directory based on current value of 'url' property | |
| 111 | return getBasePath().toFile(); | |
| 112 | } | |
| 113 | ||
| 114 | protected void updateUrl( File file ) { | |
| 115 | String newUrl; | |
| 116 | try { | |
| 117 | newUrl = getBasePath().relativize( file.toPath() ).toString(); | |
| 118 | } catch( IllegalArgumentException ex ) { | |
| 119 | newUrl = file.toString(); | |
| 120 | } | |
| 121 | url.set( newUrl.replace( '\\', '/' ) ); | |
| 122 | } | |
| 123 | } | |
| 1 | 124 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | package com.keenwrite.controls; | |
| 29 | ||
| 30 | import javafx.beans.property.SimpleStringProperty; | |
| 31 | import javafx.beans.property.StringProperty; | |
| 32 | import javafx.scene.control.TextField; | |
| 33 | import javafx.util.StringConverter; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for escaping/unescaping characters for markdown. | |
| 37 | */ | |
| 38 | public class EscapeTextField extends TextField { | |
| 39 | ||
| 40 | public EscapeTextField() { | |
| 41 | escapedText.bindBidirectional( | |
| 42 | textProperty(), | |
| 43 | new StringConverter<>() { | |
| 44 | @Override | |
| 45 | public String toString( String object ) { | |
| 46 | return escape( object ); | |
| 47 | } | |
| 48 | ||
| 49 | @Override | |
| 50 | public String fromString( String string ) { | |
| 51 | return unescape( string ); | |
| 52 | } | |
| 53 | } | |
| 54 | ); | |
| 55 | escapeCharacters.addListener( | |
| 56 | e -> escapedText.set( escape( textProperty().get() ) ) | |
| 57 | ); | |
| 58 | } | |
| 59 | ||
| 60 | // 'escapedText' property | |
| 61 | private final StringProperty escapedText = new SimpleStringProperty(); | |
| 62 | ||
| 63 | public StringProperty escapedTextProperty() { | |
| 64 | return escapedText; | |
| 65 | } | |
| 66 | ||
| 67 | // 'escapeCharacters' property | |
| 68 | private final StringProperty escapeCharacters = new SimpleStringProperty(); | |
| 69 | ||
| 70 | public String getEscapeCharacters() { | |
| 71 | return escapeCharacters.get(); | |
| 72 | } | |
| 73 | ||
| 74 | public void setEscapeCharacters( String escapeCharacters ) { | |
| 75 | this.escapeCharacters.set( escapeCharacters ); | |
| 76 | } | |
| 77 | ||
| 78 | private String escape( final String s ) { | |
| 79 | final String escapeChars = getEscapeCharacters(); | |
| 80 | ||
| 81 | return isEmpty( escapeChars ) ? s : | |
| 82 | s.replaceAll( "([" + escapeChars.replaceAll( | |
| 83 | "(.)", | |
| 84 | "\\\\$1" ) + "])", "\\\\$1" ); | |
| 85 | } | |
| 86 | ||
| 87 | private String unescape( final String s ) { | |
| 88 | final String escapeChars = getEscapeCharacters(); | |
| 89 | ||
| 90 | return isEmpty( escapeChars ) ? s : | |
| 91 | s.replaceAll( "\\\\([" + escapeChars | |
| 92 | .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" ); | |
| 93 | } | |
| 94 | ||
| 95 | private static boolean isEmpty( final String s ) { | |
| 96 | return s == null || s.isEmpty(); | |
| 97 | } | |
| 98 | } | |
| 1 | 99 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import com.keenwrite.AbstractFileFactory; | |
| 31 | import com.keenwrite.FileType; | |
| 32 | import com.keenwrite.definition.yaml.YamlDefinitionSource; | |
| 33 | ||
| 34 | import java.nio.file.Path; | |
| 35 | ||
| 36 | import static com.keenwrite.Constants.GLOB_PREFIX_DEFINITION; | |
| 37 | import static com.keenwrite.FileType.YAML; | |
| 38 | import static com.keenwrite.util.ProtocolResolver.getProtocol; | |
| 39 | ||
| 40 | /** | |
| 41 | * Responsible for creating objects that can read and write definition data | |
| 42 | * sources. The data source could be YAML, TOML, JSON, flat files, or from a | |
| 43 | * database. | |
| 44 | */ | |
| 45 | public class DefinitionFactory extends AbstractFileFactory { | |
| 46 | ||
| 47 | /** | |
| 48 | * Default (empty) constructor. | |
| 49 | */ | |
| 50 | public DefinitionFactory() { | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * Creates a definition source capable of reading definitions from the given | |
| 55 | * path. | |
| 56 | * | |
| 57 | * @param path Path to a resource containing definitions. | |
| 58 | * @return The definition source appropriate for the given path. | |
| 59 | */ | |
| 60 | public DefinitionSource createDefinitionSource( final Path path ) { | |
| 61 | assert path != null; | |
| 62 | ||
| 63 | final var protocol = getProtocol( path.toString() ); | |
| 64 | DefinitionSource result = null; | |
| 65 | ||
| 66 | if( protocol.isFile() ) { | |
| 67 | final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION ); | |
| 68 | result = createFileDefinitionSource( filetype, path ); | |
| 69 | } | |
| 70 | else { | |
| 71 | unknownFileType( protocol, path.toString() ); | |
| 72 | } | |
| 73 | ||
| 74 | return result; | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Creates a definition source based on the file type. | |
| 79 | * | |
| 80 | * @param filetype Property key name suffix from settings.properties file. | |
| 81 | * @param path Path to the file that corresponds to the extension. | |
| 82 | * @return A DefinitionSource capable of parsing the data stored at the path. | |
| 83 | */ | |
| 84 | private DefinitionSource createFileDefinitionSource( | |
| 85 | final FileType filetype, final Path path ) { | |
| 86 | assert filetype != null; | |
| 87 | assert path != null; | |
| 88 | ||
| 89 | if( filetype == YAML ) { | |
| 90 | return new YamlDefinitionSource( path ); | |
| 91 | } | |
| 92 | ||
| 93 | throw new IllegalArgumentException( filetype.toString() ); | |
| 94 | } | |
| 95 | } | |
| 1 | 96 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 31 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 32 | import javafx.beans.property.SimpleStringProperty; | |
| 33 | import javafx.beans.property.StringProperty; | |
| 34 | import javafx.collections.ObservableList; | |
| 35 | import javafx.event.ActionEvent; | |
| 36 | import javafx.event.Event; | |
| 37 | import javafx.event.EventHandler; | |
| 38 | import javafx.geometry.Insets; | |
| 39 | import javafx.geometry.Pos; | |
| 40 | import javafx.scene.Node; | |
| 41 | import javafx.scene.control.*; | |
| 42 | import javafx.scene.input.KeyEvent; | |
| 43 | import javafx.scene.layout.BorderPane; | |
| 44 | import javafx.scene.layout.HBox; | |
| 45 | import javafx.util.StringConverter; | |
| 46 | ||
| 47 | import java.util.*; | |
| 48 | ||
| 49 | import static com.keenwrite.Messages.get; | |
| 50 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 51 | import static javafx.geometry.Pos.CENTER; | |
| 52 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 53 | ||
| 54 | /** | |
| 55 | * Provides the user interface that holds a {@link TreeView}, which | |
| 56 | * allows users to interact with key/value pairs loaded from the | |
| 57 | * {@link DocumentParser} and adapted using a {@link TreeAdapter}. | |
| 58 | */ | |
| 59 | public final class DefinitionPane extends BorderPane { | |
| 60 | ||
| 61 | /** | |
| 62 | * Contains a view of the definitions. | |
| 63 | */ | |
| 64 | private final TreeView<String> mTreeView = new TreeView<>(); | |
| 65 | ||
| 66 | /** | |
| 67 | * Handlers for key press events. | |
| 68 | */ | |
| 69 | private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers | |
| 70 | = new HashSet<>(); | |
| 71 | ||
| 72 | /** | |
| 73 | * Definition file name shown in the title of the pane. | |
| 74 | */ | |
| 75 | private final StringProperty mFilename = new SimpleStringProperty(); | |
| 76 | ||
| 77 | private final TitledPane mTitledPane = new TitledPane(); | |
| 78 | ||
| 79 | /** | |
| 80 | * Constructs a definition pane with a given tree view root. | |
| 81 | */ | |
| 82 | public DefinitionPane() { | |
| 83 | final var treeView = getTreeView(); | |
| 84 | treeView.setEditable( true ); | |
| 85 | treeView.setCellFactory( cell -> createTreeCell() ); | |
| 86 | treeView.setContextMenu( createContextMenu() ); | |
| 87 | treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter ); | |
| 88 | treeView.setShowRoot( false ); | |
| 89 | getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE ); | |
| 90 | ||
| 91 | final var bCreate = createButton( | |
| 92 | "create", TREE, e -> addItem() ); | |
| 93 | final var bRename = createButton( | |
| 94 | "rename", EDIT, e -> editSelectedItem() ); | |
| 95 | final var bDelete = createButton( | |
| 96 | "delete", TRASH, e -> deleteSelectedItems() ); | |
| 97 | ||
| 98 | final var buttonBar = new HBox(); | |
| 99 | buttonBar.getChildren().addAll( bCreate, bRename, bDelete ); | |
| 100 | buttonBar.setAlignment( CENTER ); | |
| 101 | buttonBar.setSpacing( 10 ); | |
| 102 | ||
| 103 | final var titledPane = getTitledPane(); | |
| 104 | titledPane.textProperty().bind( mFilename ); | |
| 105 | titledPane.setContent( treeView ); | |
| 106 | titledPane.setCollapsible( false ); | |
| 107 | titledPane.setPadding( new Insets( 0, 0, 0, 0 ) ); | |
| 108 | ||
| 109 | setTop( buttonBar ); | |
| 110 | setCenter( titledPane ); | |
| 111 | setAlignment( buttonBar, Pos.TOP_CENTER ); | |
| 112 | setAlignment( titledPane, Pos.TOP_CENTER ); | |
| 113 | ||
| 114 | titledPane.prefHeightProperty().bind( this.heightProperty() ); | |
| 115 | } | |
| 116 | ||
| 117 | public void setTooltip( final Tooltip tooltip ) { | |
| 118 | getTitledPane().setTooltip( tooltip ); | |
| 119 | } | |
| 120 | ||
| 121 | private TitledPane getTitledPane() { | |
| 122 | return mTitledPane; | |
| 123 | } | |
| 124 | ||
| 125 | private Button createButton( | |
| 126 | final String msgKey, | |
| 127 | final FontAwesomeIcon icon, | |
| 128 | final EventHandler<ActionEvent> eventHandler ) { | |
| 129 | final var keyPrefix = "Pane.definition.button." + msgKey; | |
| 130 | final var button = new Button( get( keyPrefix + ".label" ) ); | |
| 131 | button.setOnAction( eventHandler ); | |
| 132 | ||
| 133 | button.setGraphic( | |
| 134 | FontAwesomeIconFactory.get().createIcon( icon ) | |
| 135 | ); | |
| 136 | button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) ); | |
| 137 | ||
| 138 | return button; | |
| 139 | } | |
| 140 | ||
| 141 | /** | |
| 142 | * Changes the root of the {@link TreeView} to the root of the | |
| 143 | * {@link TreeView} from the {@link DefinitionSource}. | |
| 144 | * | |
| 145 | * @param definitionSource Container for the hierarchy of key/value pairs | |
| 146 | * to replace the existing hierarchy. | |
| 147 | */ | |
| 148 | public void update( final DefinitionSource definitionSource ) { | |
| 149 | assert definitionSource != null; | |
| 150 | ||
| 151 | final TreeAdapter treeAdapter = definitionSource.getTreeAdapter(); | |
| 152 | final TreeItem<String> root = treeAdapter.adapt( | |
| 153 | get( "Pane.definition.node.root.title" ) | |
| 154 | ); | |
| 155 | ||
| 156 | getTreeView().setRoot( root ); | |
| 157 | } | |
| 158 | ||
| 159 | public Map<String, String> toMap() { | |
| 160 | return TreeItemAdapter.toMap( getTreeView().getRoot() ); | |
| 161 | } | |
| 162 | ||
| 163 | /** | |
| 164 | * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView} | |
| 165 | * is modified. The modifications include: item value changes, item additions, | |
| 166 | * and item removals. | |
| 167 | * <p> | |
| 168 | * Safe to call multiple times; if a handler is already registered, the | |
| 169 | * old handler is used. | |
| 170 | * </p> | |
| 171 | * | |
| 172 | * @param handler The handler to call whenever any {@link TreeItem} changes. | |
| 173 | */ | |
| 174 | public void addTreeChangeHandler( | |
| 175 | final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) { | |
| 176 | final TreeItem<String> root = getTreeView().getRoot(); | |
| 177 | root.addEventHandler( TreeItem.valueChangedEvent(), handler ); | |
| 178 | root.addEventHandler( TreeItem.childrenModificationEvent(), handler ); | |
| 179 | } | |
| 180 | ||
| 181 | public void addKeyEventHandler( | |
| 182 | final EventHandler<? super KeyEvent> handler ) { | |
| 183 | getKeyEventHandlers().add( handler ); | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably | |
| 188 | * well-formed for export. A tree is considered well-formed if the following | |
| 189 | * conditions are met: | |
| 190 | * | |
| 191 | * <ul> | |
| 192 | * <li>The root node contains at least one child node having a leaf.</li> | |
| 193 | * <li>There are no leaf nodes with sibling leaf nodes.</li> | |
| 194 | * </ul> | |
| 195 | * | |
| 196 | * @return {@code null} if the document is well-formed, otherwise the | |
| 197 | * problematic child {@link TreeItem}. | |
| 198 | */ | |
| 199 | public TreeItem<String> isTreeWellFormed() { | |
| 200 | final var root = getTreeView().getRoot(); | |
| 201 | ||
| 202 | for( final var child : root.getChildren() ) { | |
| 203 | final var problemChild = isWellFormed( child ); | |
| 204 | ||
| 205 | if( child.isLeaf() || problemChild != null ) { | |
| 206 | return problemChild; | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | return null; | |
| 211 | } | |
| 212 | ||
| 213 | /** | |
| 214 | * Determines whether the document is well-formed by ensuring that | |
| 215 | * child branches do not contain multiple leaves. | |
| 216 | * | |
| 217 | * @param item The sub-tree to check for well-formedness. | |
| 218 | * @return {@code null} when the tree is well-formed, otherwise the | |
| 219 | * problematic {@link TreeItem}. | |
| 220 | */ | |
| 221 | private TreeItem<String> isWellFormed( final TreeItem<String> item ) { | |
| 222 | int childLeafs = 0; | |
| 223 | int childBranches = 0; | |
| 224 | ||
| 225 | for( final TreeItem<String> child : item.getChildren() ) { | |
| 226 | if( child.isLeaf() ) { | |
| 227 | childLeafs++; | |
| 228 | } | |
| 229 | else { | |
| 230 | childBranches++; | |
| 231 | } | |
| 232 | ||
| 233 | final var problemChild = isWellFormed( child ); | |
| 234 | ||
| 235 | if( problemChild != null ) { | |
| 236 | return problemChild; | |
| 237 | } | |
| 238 | } | |
| 239 | ||
| 240 | return ((childBranches > 0 && childLeafs == 0) || | |
| 241 | (childBranches == 0 && childLeafs <= 1)) ? null : item; | |
| 242 | } | |
| 243 | ||
| 244 | /** | |
| 245 | * Delegates to {@link DefinitionTreeItem#findLeafExact(String)}. | |
| 246 | * | |
| 247 | * @param text The value to find, never {@code null}. | |
| 248 | * @return The leaf that contains the given value, or {@code null} if | |
| 249 | * not found. | |
| 250 | */ | |
| 251 | public DefinitionTreeItem<String> findLeafExact( final String text ) { | |
| 252 | return getTreeRoot().findLeafExact( text ); | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}. | |
| 257 | * | |
| 258 | * @param text The value to find, never {@code null}. | |
| 259 | * @return The leaf that contains the given value, or {@code null} if | |
| 260 | * not found. | |
| 261 | */ | |
| 262 | public DefinitionTreeItem<String> findLeafContains( final String text ) { | |
| 263 | return getTreeRoot().findLeafContains( text ); | |
| 264 | } | |
| 265 | ||
| 266 | /** | |
| 267 | * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}. | |
| 268 | * | |
| 269 | * @param text The value to find, never {@code null}. | |
| 270 | * @return The leaf that contains the given value, or {@code null} if | |
| 271 | * not found. | |
| 272 | */ | |
| 273 | public DefinitionTreeItem<String> findLeafContainsNoCase( | |
| 274 | final String text ) { | |
| 275 | return getTreeRoot().findLeafContainsNoCase( text ); | |
| 276 | } | |
| 277 | ||
| 278 | /** | |
| 279 | * Delegates to {@link DefinitionTreeItem#findLeafStartsWith(String)}. | |
| 280 | * | |
| 281 | * @param text The value to find, never {@code null}. | |
| 282 | * @return The leaf that contains the given value, or {@code null} if | |
| 283 | * not found. | |
| 284 | */ | |
| 285 | public DefinitionTreeItem<String> findLeafStartsWith( final String text ) { | |
| 286 | return getTreeRoot().findLeafStartsWith( text ); | |
| 287 | } | |
| 288 | ||
| 289 | /** | |
| 290 | * Expands the node to the root, recursively. | |
| 291 | * | |
| 292 | * @param <T> The type of tree item to expand (usually String). | |
| 293 | * @param node The node to expand. | |
| 294 | */ | |
| 295 | public <T> void expand( final TreeItem<T> node ) { | |
| 296 | if( node != null ) { | |
| 297 | expand( node.getParent() ); | |
| 298 | ||
| 299 | if( !node.isLeaf() ) { | |
| 300 | node.setExpanded( true ); | |
| 301 | } | |
| 302 | } | |
| 303 | } | |
| 304 | ||
| 305 | public void select( final TreeItem<String> item ) { | |
| 306 | getSelectionModel().clearSelection(); | |
| 307 | getSelectionModel().select( getTreeView().getRow( item ) ); | |
| 308 | } | |
| 309 | ||
| 310 | /** | |
| 311 | * Collapses the tree, recursively. | |
| 312 | */ | |
| 313 | public void collapse() { | |
| 314 | collapse( getTreeRoot().getChildren() ); | |
| 315 | } | |
| 316 | ||
| 317 | /** | |
| 318 | * Collapses the tree, recursively. | |
| 319 | * | |
| 320 | * @param <T> The type of tree item to expand (usually String). | |
| 321 | * @param nodes The nodes to collapse. | |
| 322 | */ | |
| 323 | private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) { | |
| 324 | for( final var node : nodes ) { | |
| 325 | node.setExpanded( false ); | |
| 326 | collapse( node.getChildren() ); | |
| 327 | } | |
| 328 | } | |
| 329 | ||
| 330 | /** | |
| 331 | * @return {@code true} when the user is editing a {@link TreeItem}. | |
| 332 | */ | |
| 333 | private boolean isEditingTreeItem() { | |
| 334 | return getTreeView().editingItemProperty().getValue() != null; | |
| 335 | } | |
| 336 | ||
| 337 | /** | |
| 338 | * Changes to edit mode for the selected item. | |
| 339 | */ | |
| 340 | private void editSelectedItem() { | |
| 341 | getTreeView().edit( getSelectedItem() ); | |
| 342 | } | |
| 343 | ||
| 344 | /** | |
| 345 | * Removes all selected items from the {@link TreeView}. | |
| 346 | */ | |
| 347 | private void deleteSelectedItems() { | |
| 348 | for( final var item : getSelectedItems() ) { | |
| 349 | final var parent = item.getParent(); | |
| 350 | ||
| 351 | if( parent != null ) { | |
| 352 | parent.getChildren().remove( item ); | |
| 353 | } | |
| 354 | } | |
| 355 | } | |
| 356 | ||
| 357 | /** | |
| 358 | * Deletes the selected item. | |
| 359 | */ | |
| 360 | private void deleteSelectedItem() { | |
| 361 | final var c = getSelectedItem(); | |
| 362 | getSiblings( c ).remove( c ); | |
| 363 | } | |
| 364 | ||
| 365 | /** | |
| 366 | * Adds a new item under the selected item (or root if nothing is selected). | |
| 367 | * There are a few conditions to consider: when adding to the root, | |
| 368 | * when adding to a leaf, and when adding to a non-leaf. Items added to the | |
| 369 | * root must contain two items: a key and a value. | |
| 370 | */ | |
| 371 | public void addItem() { | |
| 372 | final var value = createTreeItem(); | |
| 373 | getSelectedItem().getChildren().add( value ); | |
| 374 | expand( value ); | |
| 375 | select( value ); | |
| 376 | } | |
| 377 | ||
| 378 | private ContextMenu createContextMenu() { | |
| 379 | final ContextMenu menu = new ContextMenu(); | |
| 380 | final ObservableList<MenuItem> items = menu.getItems(); | |
| 381 | ||
| 382 | addMenuItem( items, "Definition.menu.create" ) | |
| 383 | .setOnAction( e -> addItem() ); | |
| 384 | ||
| 385 | addMenuItem( items, "Definition.menu.rename" ) | |
| 386 | .setOnAction( e -> editSelectedItem() ); | |
| 387 | ||
| 388 | addMenuItem( items, "Definition.menu.remove" ) | |
| 389 | .setOnAction( e -> deleteSelectedItem() ); | |
| 390 | ||
| 391 | return menu; | |
| 392 | } | |
| 393 | ||
| 394 | /** | |
| 395 | * Executes hot-keys for edits to the definition tree. | |
| 396 | * | |
| 397 | * @param event Contains the key code of the key that was pressed. | |
| 398 | */ | |
| 399 | private void keyEventFilter( final KeyEvent event ) { | |
| 400 | if( !isEditingTreeItem() ) { | |
| 401 | switch( event.getCode() ) { | |
| 402 | case ENTER: | |
| 403 | expand( getSelectedItem() ); | |
| 404 | event.consume(); | |
| 405 | break; | |
| 406 | ||
| 407 | case DELETE: | |
| 408 | deleteSelectedItems(); | |
| 409 | break; | |
| 410 | ||
| 411 | case INSERT: | |
| 412 | addItem(); | |
| 413 | break; | |
| 414 | ||
| 415 | case R: | |
| 416 | if( event.isControlDown() ) { | |
| 417 | editSelectedItem(); | |
| 418 | } | |
| 419 | ||
| 420 | break; | |
| 421 | } | |
| 422 | ||
| 423 | for( final var handler : getKeyEventHandlers() ) { | |
| 424 | handler.handle( event ); | |
| 425 | } | |
| 426 | } | |
| 427 | } | |
| 428 | ||
| 429 | /** | |
| 430 | * Adds a menu item to a list of menu items. | |
| 431 | * | |
| 432 | * @param items The list of menu items to append to. | |
| 433 | * @param labelKey The resource bundle key name for the menu item's label. | |
| 434 | * @return The menu item added to the list of menu items. | |
| 435 | */ | |
| 436 | private MenuItem addMenuItem( | |
| 437 | final List<MenuItem> items, final String labelKey ) { | |
| 438 | final MenuItem menuItem = createMenuItem( labelKey ); | |
| 439 | items.add( menuItem ); | |
| 440 | return menuItem; | |
| 441 | } | |
| 442 | ||
| 443 | private MenuItem createMenuItem( final String labelKey ) { | |
| 444 | return new MenuItem( get( labelKey ) ); | |
| 445 | } | |
| 446 | ||
| 447 | private DefinitionTreeItem<String> createTreeItem() { | |
| 448 | return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) ); | |
| 449 | } | |
| 450 | ||
| 451 | private TreeCell<String> createTreeCell() { | |
| 452 | return new FocusAwareTextFieldTreeCell( createStringConverter() ) { | |
| 453 | @Override | |
| 454 | public void commitEdit( final String newValue ) { | |
| 455 | super.commitEdit( newValue ); | |
| 456 | select( getTreeItem() ); | |
| 457 | requestFocus(); | |
| 458 | } | |
| 459 | }; | |
| 460 | } | |
| 461 | ||
| 462 | @Override | |
| 463 | public void requestFocus() { | |
| 464 | super.requestFocus(); | |
| 465 | getTreeView().requestFocus(); | |
| 466 | } | |
| 467 | ||
| 468 | private StringConverter<String> createStringConverter() { | |
| 469 | return new StringConverter<>() { | |
| 470 | @Override | |
| 471 | public String toString( final String object ) { | |
| 472 | return object == null ? "" : object; | |
| 473 | } | |
| 474 | ||
| 475 | @Override | |
| 476 | public String fromString( final String string ) { | |
| 477 | return string == null ? "" : string; | |
| 478 | } | |
| 479 | }; | |
| 480 | } | |
| 481 | ||
| 482 | /** | |
| 483 | * Returns the tree view that contains the definition hierarchy. | |
| 484 | * | |
| 485 | * @return A non-null instance. | |
| 486 | */ | |
| 487 | public TreeView<String> getTreeView() { | |
| 488 | return mTreeView; | |
| 489 | } | |
| 490 | ||
| 491 | /** | |
| 492 | * Returns this pane. | |
| 493 | * | |
| 494 | * @return this | |
| 495 | */ | |
| 496 | public Node getNode() { | |
| 497 | return this; | |
| 498 | } | |
| 499 | ||
| 500 | /** | |
| 501 | * Returns the property used to set the title of the pane: the file name. | |
| 502 | * | |
| 503 | * @return A non-null property used for showing the definition file name. | |
| 504 | */ | |
| 505 | public StringProperty filenameProperty() { | |
| 506 | return mFilename; | |
| 507 | } | |
| 508 | ||
| 509 | /** | |
| 510 | * Returns the root of the tree. | |
| 511 | * | |
| 512 | * @return The first node added to the definition tree. | |
| 513 | */ | |
| 514 | private DefinitionTreeItem<String> getTreeRoot() { | |
| 515 | final var root = getTreeView().getRoot(); | |
| 516 | ||
| 517 | return root instanceof DefinitionTreeItem | |
| 518 | ? (DefinitionTreeItem<String>) root | |
| 519 | : new DefinitionTreeItem<>( "root" ); | |
| 520 | } | |
| 521 | ||
| 522 | private ObservableList<TreeItem<String>> getSiblings( | |
| 523 | final TreeItem<String> item ) { | |
| 524 | final var root = getTreeView().getRoot(); | |
| 525 | final var parent = (item == null || item == root) ? root : item.getParent(); | |
| 526 | ||
| 527 | return parent.getChildren(); | |
| 528 | } | |
| 529 | ||
| 530 | private MultipleSelectionModel<TreeItem<String>> getSelectionModel() { | |
| 531 | return getTreeView().getSelectionModel(); | |
| 532 | } | |
| 533 | ||
| 534 | /** | |
| 535 | * Returns a copy of all the selected items. | |
| 536 | * | |
| 537 | * @return A list, possibly empty, containing all selected items in the | |
| 538 | * {@link TreeView}. | |
| 539 | */ | |
| 540 | private List<TreeItem<String>> getSelectedItems() { | |
| 541 | return new ArrayList<>( getSelectionModel().getSelectedItems() ); | |
| 542 | } | |
| 543 | ||
| 544 | public TreeItem<String> getSelectedItem() { | |
| 545 | final var item = getSelectionModel().getSelectedItem(); | |
| 546 | return item == null ? getTreeView().getRoot() : item; | |
| 547 | } | |
| 548 | ||
| 549 | private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() { | |
| 550 | return mKeyEventHandlers; | |
| 551 | } | |
| 552 | ||
| 553 | /** | |
| 554 | * Answers whether there are any definitions in the tree. | |
| 555 | * | |
| 556 | * @return {@code true} when there are no definitions; {@code false} when | |
| 557 | * there's at least one definition. | |
| 558 | */ | |
| 559 | public boolean isEmpty() { | |
| 560 | return getTreeRoot().isEmpty(); | |
| 561 | } | |
| 562 | } | |
| 1 | 563 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents behaviours for reading and writing string definitions. This | |
| 32 | * class cannot have any direct hooks into the user interface, as it defines | |
| 33 | * entry points into the definition data model loaded into an object | |
| 34 | * hierarchy. That hierarchy is converted to a UI model using an adapter | |
| 35 | * pattern. | |
| 36 | */ | |
| 37 | public interface DefinitionSource { | |
| 38 | ||
| 39 | /** | |
| 40 | * Creates an object capable of producing view-based objects from this | |
| 41 | * definition source. | |
| 42 | * | |
| 43 | * @return A hierarchical tree suitable for displaying in the definition pane. | |
| 44 | */ | |
| 45 | TreeAdapter getTreeAdapter(); | |
| 46 | } | |
| 1 | 47 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | ||
| 32 | import java.util.Stack; | |
| 33 | import java.util.function.BiFunction; | |
| 34 | ||
| 35 | import static java.text.Normalizer.Form.NFD; | |
| 36 | import static java.text.Normalizer.normalize; | |
| 37 | ||
| 38 | /** | |
| 39 | * Provides behaviour afforded to definition keys and corresponding value. | |
| 40 | * | |
| 41 | * @param <T> The type of {@link TreeItem} (usually string). | |
| 42 | */ | |
| 43 | public class DefinitionTreeItem<T> extends TreeItem<T> { | |
| 44 | ||
| 45 | /** | |
| 46 | * Constructs a new item with a default value. | |
| 47 | * | |
| 48 | * @param value Passed up to superclass. | |
| 49 | */ | |
| 50 | public DefinitionTreeItem( final T value ) { | |
| 51 | super( value ); | |
| 52 | } | |
| 53 | ||
| 54 | /** | |
| 55 | * Finds a leaf starting at the current node with text that matches the given | |
| 56 | * value. Search is performed case-sensitively. | |
| 57 | * | |
| 58 | * @param text The text to match against each leaf in the tree. | |
| 59 | * @return The leaf that has a value exactly matching the given text. | |
| 60 | */ | |
| 61 | public DefinitionTreeItem<T> findLeafExact( final String text ) { | |
| 62 | return findLeaf( text, DefinitionTreeItem::valueEquals ); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Finds a leaf starting at the current node with text that matches the given | |
| 67 | * value. Search is performed case-sensitively. | |
| 68 | * | |
| 69 | * @param text The text to match against each leaf in the tree. | |
| 70 | * @return The leaf that has a value that contains the given text. | |
| 71 | */ | |
| 72 | public DefinitionTreeItem<T> findLeafContains( final String text ) { | |
| 73 | return findLeaf( text, DefinitionTreeItem::valueContains ); | |
| 74 | } | |
| 75 | ||
| 76 | /** | |
| 77 | * Finds a leaf starting at the current node with text that matches the given | |
| 78 | * value. Search is performed case-insensitively. | |
| 79 | * | |
| 80 | * @param text The text to match against each leaf in the tree. | |
| 81 | * @return The leaf that has a value that contains the given text. | |
| 82 | */ | |
| 83 | public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) { | |
| 84 | return findLeaf( text, DefinitionTreeItem::valueContainsNoCase ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Finds a leaf starting at the current node with text that matches the given | |
| 89 | * value. Search is performed case-sensitively. | |
| 90 | * | |
| 91 | * @param text The text to match against each leaf in the tree. | |
| 92 | * @return The leaf that has a value that starts with the given text. | |
| 93 | */ | |
| 94 | public DefinitionTreeItem<T> findLeafStartsWith( final String text ) { | |
| 95 | return findLeaf( text, DefinitionTreeItem::valueStartsWith ); | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Finds a leaf starting at the current node with text that matches the given | |
| 100 | * value. | |
| 101 | * | |
| 102 | * @param text The text to match against each leaf in the tree. | |
| 103 | * @param findMode What algorithm is used to match the given text. | |
| 104 | * @return The leaf that has a value starting with the given text, or {@code | |
| 105 | * null} if there was no match found. | |
| 106 | */ | |
| 107 | public DefinitionTreeItem<T> findLeaf( | |
| 108 | final String text, | |
| 109 | final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) { | |
| 110 | final var stack = new Stack<DefinitionTreeItem<T>>(); | |
| 111 | stack.push( this ); | |
| 112 | ||
| 113 | // Don't hunt for blank (empty) keys. | |
| 114 | boolean found = text.isBlank(); | |
| 115 | ||
| 116 | while( !found && !stack.isEmpty() ) { | |
| 117 | final var node = stack.pop(); | |
| 118 | ||
| 119 | for( final var child : node.getChildren() ) { | |
| 120 | final var result = (DefinitionTreeItem<T>) child; | |
| 121 | ||
| 122 | if( result.isLeaf() ) { | |
| 123 | if( found = findMode.apply( result, text ) ) { | |
| 124 | return result; | |
| 125 | } | |
| 126 | } | |
| 127 | else { | |
| 128 | stack.push( result ); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | return null; | |
| 134 | } | |
| 135 | ||
| 136 | /** | |
| 137 | * Returns the value of the string without diacritic marks. | |
| 138 | * | |
| 139 | * @return A non-null, possibly empty string. | |
| 140 | */ | |
| 141 | private String getDiacriticlessValue() { | |
| 142 | return normalize( getValue().toString(), NFD ) | |
| 143 | .replaceAll( "\\p{M}", "" ); | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Returns true if this node is a leaf and its value equals the given text. | |
| 148 | * | |
| 149 | * @param s The text to compare against the node value. | |
| 150 | * @return true Node is a leaf and its value equals the given value. | |
| 151 | */ | |
| 152 | private boolean valueEquals( final String s ) { | |
| 153 | return isLeaf() && getValue().equals( s ); | |
| 154 | } | |
| 155 | ||
| 156 | /** | |
| 157 | * Returns true if this node is a leaf and its value contains the given text. | |
| 158 | * | |
| 159 | * @param s The text to compare against the node value. | |
| 160 | * @return true Node is a leaf and its value contains the given value. | |
| 161 | */ | |
| 162 | private boolean valueContains( final String s ) { | |
| 163 | return isLeaf() && getDiacriticlessValue().contains( s ); | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Returns true if this node is a leaf and its value contains the given text. | |
| 168 | * | |
| 169 | * @param s The text to compare against the node value. | |
| 170 | * @return true Node is a leaf and its value contains the given value. | |
| 171 | */ | |
| 172 | private boolean valueContainsNoCase( final String s ) { | |
| 173 | return isLeaf() && getDiacriticlessValue() | |
| 174 | .toLowerCase().contains( s.toLowerCase() ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Returns true if this node is a leaf and its value starts with the given | |
| 179 | * text. | |
| 180 | * | |
| 181 | * @param s The text to compare against the node value. | |
| 182 | * @return true Node is a leaf and its value starts with the given value. | |
| 183 | */ | |
| 184 | private boolean valueStartsWith( final String s ) { | |
| 185 | return isLeaf() && getDiacriticlessValue().startsWith( s ); | |
| 186 | } | |
| 187 | ||
| 188 | /** | |
| 189 | * Returns the path for this node, with nodes made distinct using the | |
| 190 | * separator character. This uses two loops: one for pushing nodes onto a | |
| 191 | * stack and one for popping them off to create the path in desired order. | |
| 192 | * | |
| 193 | * @return A non-null string, possibly empty. | |
| 194 | */ | |
| 195 | public String toPath() { | |
| 196 | return TreeItemAdapter.toPath( getParent() ); | |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * Answers whether there are any definitions in this tree. | |
| 201 | * | |
| 202 | * @return {@code true} when there are no definitions in the tree; {@code | |
| 203 | * false} when there is at least one definition present. | |
| 204 | */ | |
| 205 | public boolean isEmpty() { | |
| 206 | return getChildren().isEmpty(); | |
| 207 | } | |
| 208 | } | |
| 1 | 209 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | /** | |
| 31 | * Responsible for parsing structured document formats. | |
| 32 | * | |
| 33 | * @param <T> The type of "node" for the document's object model. | |
| 34 | */ | |
| 35 | public interface DocumentParser<T> { | |
| 36 | ||
| 37 | /** | |
| 38 | * Parses a document into a nested object hierarchy. The object returned | |
| 39 | * from this call must be the root node in the document tree. | |
| 40 | * | |
| 41 | * @return The document's root node, which may be empty but never null. | |
| 42 | */ | |
| 43 | T getDocumentRoot(); | |
| 44 | } | |
| 1 | 45 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import javafx.scene.Node; | |
| 31 | import javafx.scene.control.TextField; | |
| 32 | import javafx.scene.control.cell.TextFieldTreeCell; | |
| 33 | import javafx.util.StringConverter; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for fixing a focus lost bug in the JavaFX implementation. | |
| 37 | * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details. | |
| 38 | * This implementation borrows from the official documentation on creating | |
| 39 | * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm | |
| 40 | */ | |
| 41 | public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> { | |
| 42 | private TextField mTextField; | |
| 43 | ||
| 44 | public FocusAwareTextFieldTreeCell( | |
| 45 | final StringConverter<String> converter ) { | |
| 46 | super( converter ); | |
| 47 | } | |
| 48 | ||
| 49 | @Override | |
| 50 | public void startEdit() { | |
| 51 | super.startEdit(); | |
| 52 | var textField = mTextField; | |
| 53 | ||
| 54 | if( textField == null ) { | |
| 55 | textField = createTextField(); | |
| 56 | } | |
| 57 | else { | |
| 58 | textField.setText( getItem() ); | |
| 59 | } | |
| 60 | ||
| 61 | setText( null ); | |
| 62 | setGraphic( textField ); | |
| 63 | textField.selectAll(); | |
| 64 | textField.requestFocus(); | |
| 65 | ||
| 66 | // When the focus is lost, commit the edit then close the input field. | |
| 67 | // This fixes the unexpected behaviour when user clicks away. | |
| 68 | textField.focusedProperty().addListener( ( l, o, n ) -> { | |
| 69 | if( !n ) { | |
| 70 | commitEdit( mTextField.getText() ); | |
| 71 | } | |
| 72 | } ); | |
| 73 | ||
| 74 | mTextField = textField; | |
| 75 | } | |
| 76 | ||
| 77 | @Override | |
| 78 | public void cancelEdit() { | |
| 79 | super.cancelEdit(); | |
| 80 | setText( getItem() ); | |
| 81 | setGraphic( getTreeItem().getGraphic() ); | |
| 82 | } | |
| 83 | ||
| 84 | @Override | |
| 85 | public void updateItem( String item, boolean empty ) { | |
| 86 | super.updateItem( item, empty ); | |
| 87 | ||
| 88 | String text = null; | |
| 89 | Node graphic = null; | |
| 90 | ||
| 91 | if( !empty ) { | |
| 92 | if( isEditing() ) { | |
| 93 | final var textField = mTextField; | |
| 94 | ||
| 95 | if( textField != null ) { | |
| 96 | textField.setText( getString() ); | |
| 97 | } | |
| 98 | ||
| 99 | graphic = textField; | |
| 100 | } | |
| 101 | else { | |
| 102 | text = getString(); | |
| 103 | graphic = getTreeItem().getGraphic(); | |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | setText( text ); | |
| 108 | setGraphic( graphic ); | |
| 109 | } | |
| 110 | ||
| 111 | private TextField createTextField() { | |
| 112 | final var textField = new TextField( getString() ); | |
| 113 | ||
| 114 | textField.setOnKeyReleased( t -> { | |
| 115 | switch( t.getCode() ) { | |
| 116 | case ENTER -> commitEdit( textField.getText() ); | |
| 117 | case ESCAPE -> cancelEdit(); | |
| 118 | } | |
| 119 | } ); | |
| 120 | ||
| 121 | return textField; | |
| 122 | } | |
| 123 | ||
| 124 | private String getString() { | |
| 125 | return getConverter().toString( getItem() ); | |
| 126 | } | |
| 127 | } | |
| 1 | 128 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 31 | ||
| 32 | import java.util.Map; | |
| 33 | import java.util.regex.Matcher; | |
| 34 | ||
| 35 | import static com.keenwrite.sigils.YamlSigilOperator.REGEX_PATTERN; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for performing string interpolation on key/value pairs stored | |
| 39 | * in a map. The values in the map can use a delimited syntax to refer to | |
| 40 | * keys in the map. | |
| 41 | */ | |
| 42 | public class MapInterpolator { | |
| 43 | private static final int GROUP_DELIMITED = 1; | |
| 44 | ||
| 45 | /** | |
| 46 | * Empty. | |
| 47 | */ | |
| 48 | private MapInterpolator() { | |
| 49 | } | |
| 50 | ||
| 51 | /** | |
| 52 | * Performs string interpolation on the values in the given map. This will | |
| 53 | * change any value in the map that contains a variable that matches | |
| 54 | * {@link YamlSigilOperator#REGEX_PATTERN}. | |
| 55 | * | |
| 56 | * @param map Contains values that represent references to keys. | |
| 57 | */ | |
| 58 | public static void interpolate( final Map<String, String> map ) { | |
| 59 | map.replaceAll( ( k, v ) -> resolve( map, v ) ); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Given a value with zero or more key references, this will resolve all | |
| 64 | * the values, recursively. If a key cannot be dereferenced, the value will | |
| 65 | * contain the key name. | |
| 66 | * | |
| 67 | * @param map Map to search for keys when resolving key references. | |
| 68 | * @param value Value containing zero or more key references | |
| 69 | * @return The given value with all embedded key references interpolated. | |
| 70 | */ | |
| 71 | private static String resolve( | |
| 72 | final Map<String, String> map, String value ) { | |
| 73 | final Matcher matcher = REGEX_PATTERN.matcher( value ); | |
| 74 | ||
| 75 | while( matcher.find() ) { | |
| 76 | final String keyName = matcher.group( GROUP_DELIMITED ); | |
| 77 | final String mapValue = map.get( keyName ); | |
| 78 | final String keyValue = mapValue == null | |
| 79 | ? keyName | |
| 80 | : resolve( map, mapValue ); | |
| 81 | ||
| 82 | value = value.replace( keyName, keyValue ); | |
| 83 | } | |
| 84 | ||
| 85 | return value; | |
| 86 | } | |
| 87 | } | |
| 1 | 88 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | import javafx.scene.control.TreeView; | |
| 32 | ||
| 33 | /** | |
| 34 | * Indicates that this is the top-most {@link TreeItem}. This class allows | |
| 35 | * the {@link TreeItemAdapter} to ignore the topmost definition. Such | |
| 36 | * contortions are necessary because {@link TreeView} requires a root item | |
| 37 | * that isn't part of the user's definition file. | |
| 38 | * <p> | |
| 39 | * Another approach would be to associate object pairs per {@link TreeItem}, | |
| 40 | * but that would be a waste of memory since the only "exception" case is | |
| 41 | * the root {@link TreeItem}. | |
| 42 | * </p> | |
| 43 | * | |
| 44 | * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}. | |
| 45 | */ | |
| 46 | public class RootTreeItem<T> extends DefinitionTreeItem<T> { | |
| 47 | /** | |
| 48 | * Default constructor, calls the superclass, no other behaviour. | |
| 49 | * | |
| 50 | * @param value The {@link TreeItem} node name to construct the superclass. | |
| 51 | * @see TreeItemAdapter#toMap(TreeItem) for details on how this | |
| 52 | * class is used. | |
| 53 | */ | |
| 54 | public RootTreeItem( final T value ) { | |
| 55 | super( value ); | |
| 56 | } | |
| 57 | } | |
| 1 | 58 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | ||
| 32 | import java.io.IOException; | |
| 33 | import java.nio.file.Path; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for converting an object hierarchy into a {@link TreeItem} | |
| 37 | * hierarchy. | |
| 38 | */ | |
| 39 | public interface TreeAdapter { | |
| 40 | /** | |
| 41 | * Adapts the document produced by the given parser into a {@link TreeItem} | |
| 42 | * object that can be presented to the user within a GUI. | |
| 43 | * | |
| 44 | * @param root The default root node name. | |
| 45 | * @return The parsed document in a {@link TreeItem} that can be displayed | |
| 46 | * in a panel. | |
| 47 | */ | |
| 48 | TreeItem<String> adapt( String root ); | |
| 49 | ||
| 50 | /** | |
| 51 | * Exports the given root node to the given path. | |
| 52 | * | |
| 53 | * @param root The root node to export. | |
| 54 | * @param path Where to persist the data. | |
| 55 | * @throws IOException Could not write the data to the given path. | |
| 56 | */ | |
| 57 | void export( TreeItem<String> root, Path path ) throws IOException; | |
| 58 | } | |
| 1 | 59 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 32 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 33 | import javafx.scene.control.TreeItem; | |
| 34 | import javafx.scene.control.TreeView; | |
| 35 | ||
| 36 | import java.util.HashMap; | |
| 37 | import java.util.Iterator; | |
| 38 | import java.util.Map; | |
| 39 | import java.util.Stack; | |
| 40 | ||
| 41 | import static com.keenwrite.Constants.DEFAULT_MAP_SIZE; | |
| 42 | ||
| 43 | /** | |
| 44 | * Given a {@link TreeItem}, this will generate a flat map with all the | |
| 45 | * values in the tree recursively interpolated. The application integrates | |
| 46 | * definition files as follows: | |
| 47 | * <ol> | |
| 48 | * <li>Load YAML file into {@link JsonNode} hierarchy.</li> | |
| 49 | * <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li> | |
| 50 | * <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li> | |
| 51 | * <li>Substitute flat map variables into document as required.</li> | |
| 52 | * </ol> | |
| 53 | * | |
| 54 | * <p> | |
| 55 | * This class is responsible for producing the interpolated flat map. This | |
| 56 | * allows dynamic edits of the {@link TreeView} to be displayed in the | |
| 57 | * {@link HTMLPreviewPane} without having to reload the definition file. | |
| 58 | * Reloading the definition file would work, but has a number of drawbacks. | |
| 59 | * </p> | |
| 60 | */ | |
| 61 | public class TreeItemAdapter { | |
| 62 | /** | |
| 63 | * Separates YAML definition keys (e.g., the dots in {@code $root.node.var$}). | |
| 64 | */ | |
| 65 | public static final String SEPARATOR = "."; | |
| 66 | ||
| 67 | /** | |
| 68 | * Default buffer length for keys ({@link StringBuilder} has 16 character | |
| 69 | * buffer) that should be large enough for most keys to avoid reallocating | |
| 70 | * memory to increase the {@link StringBuilder}'s buffer. | |
| 71 | */ | |
| 72 | public static final int DEFAULT_KEY_LENGTH = 64; | |
| 73 | ||
| 74 | /** | |
| 75 | * In-order traversal of a {@link TreeItem} hierarchy, exposing each item | |
| 76 | * as a consecutive list. | |
| 77 | */ | |
| 78 | private static final class TreeIterator | |
| 79 | implements Iterator<TreeItem<String>> { | |
| 80 | private final Stack<TreeItem<String>> mStack = new Stack<>(); | |
| 81 | ||
| 82 | public TreeIterator( final TreeItem<String> root ) { | |
| 83 | if( root != null ) { | |
| 84 | mStack.push( root ); | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | @Override | |
| 89 | public boolean hasNext() { | |
| 90 | return !mStack.isEmpty(); | |
| 91 | } | |
| 92 | ||
| 93 | @Override | |
| 94 | public TreeItem<String> next() { | |
| 95 | final TreeItem<String> next = mStack.pop(); | |
| 96 | next.getChildren().forEach( mStack::push ); | |
| 97 | ||
| 98 | return next; | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | private TreeItemAdapter() { | |
| 103 | } | |
| 104 | ||
| 105 | /** | |
| 106 | * Iterate over a given root node (at any level of the tree) and process each | |
| 107 | * leaf node into a flat map. Values must be interpolated separately. | |
| 108 | */ | |
| 109 | public static Map<String, String> toMap( final TreeItem<String> root ) { | |
| 110 | final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE ); | |
| 111 | final TreeIterator iterator = new TreeIterator( root ); | |
| 112 | ||
| 113 | iterator.forEachRemaining( item -> { | |
| 114 | if( item.isLeaf() ) { | |
| 115 | map.put( toPath( item.getParent() ), item.getValue() ); | |
| 116 | } | |
| 117 | } ); | |
| 118 | ||
| 119 | return map; | |
| 120 | } | |
| 121 | ||
| 122 | ||
| 123 | /** | |
| 124 | * For a given node, this will ascend the tree to generate a key name | |
| 125 | * that is associated with the leaf node's value. | |
| 126 | * | |
| 127 | * @param node Ascendants represent the key to this node's value. | |
| 128 | * @param <T> Data type that the {@link TreeItem} contains. | |
| 129 | * @return The string representation of the node's unique key. | |
| 130 | */ | |
| 131 | public static <T> String toPath( TreeItem<T> node ) { | |
| 132 | assert node != null; | |
| 133 | ||
| 134 | final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH ); | |
| 135 | final Stack<TreeItem<T>> stack = new Stack<>(); | |
| 136 | ||
| 137 | while( node != null && !(node instanceof RootTreeItem) ) { | |
| 138 | stack.push( node ); | |
| 139 | node = node.getParent(); | |
| 140 | } | |
| 141 | ||
| 142 | // Gets set at end of first iteration (to avoid an if condition). | |
| 143 | String separator = ""; | |
| 144 | ||
| 145 | while( !stack.empty() ) { | |
| 146 | final T subkey = stack.pop().getValue(); | |
| 147 | key.append( separator ); | |
| 148 | key.append( subkey ); | |
| 149 | separator = SEPARATOR; | |
| 150 | } | |
| 151 | ||
| 152 | return YamlSigilOperator.entoken( key.toString() ); | |
| 153 | } | |
| 154 | } | |
| 1 | 155 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition.yaml; | |
| 29 | ||
| 30 | import com.keenwrite.definition.DefinitionSource; | |
| 31 | import com.keenwrite.definition.TreeAdapter; | |
| 32 | ||
| 33 | import java.nio.file.Path; | |
| 34 | ||
| 35 | /** | |
| 36 | * Represents a definition data source for YAML files. | |
| 37 | */ | |
| 38 | public class YamlDefinitionSource implements DefinitionSource { | |
| 39 | ||
| 40 | private final YamlTreeAdapter mYamlTreeAdapter; | |
| 41 | ||
| 42 | /** | |
| 43 | * Constructs a new YAML definition source, populated from the given file. | |
| 44 | * | |
| 45 | * @param path Path to the YAML definition file. | |
| 46 | */ | |
| 47 | public YamlDefinitionSource( final Path path ) { | |
| 48 | assert path != null; | |
| 49 | ||
| 50 | mYamlTreeAdapter = new YamlTreeAdapter( path ); | |
| 51 | } | |
| 52 | ||
| 53 | @Override | |
| 54 | public TreeAdapter getTreeAdapter() { | |
| 55 | return mYamlTreeAdapter; | |
| 56 | } | |
| 57 | } | |
| 1 | 58 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition.yaml; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 32 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; | |
| 33 | import com.keenwrite.definition.DocumentParser; | |
| 34 | ||
| 35 | import java.io.InputStream; | |
| 36 | import java.nio.file.Files; | |
| 37 | import java.nio.file.Path; | |
| 38 | ||
| 39 | /** | |
| 40 | * Responsible for reading a YAML document into an object hierarchy. | |
| 41 | */ | |
| 42 | public class YamlParser implements DocumentParser<JsonNode> { | |
| 43 | ||
| 44 | /** | |
| 45 | * Start of the Universe (the YAML document node that contains all others). | |
| 46 | */ | |
| 47 | private final JsonNode mDocumentRoot; | |
| 48 | ||
| 49 | /** | |
| 50 | * Creates a new YamlParser instance that attempts to parse the contents | |
| 51 | * of the YAML document given from a path. In the event that the file either | |
| 52 | * does not exist or is empty, a fake | |
| 53 | * | |
| 54 | * @param path Path to a file containing YAML data to parse. | |
| 55 | */ | |
| 56 | public YamlParser( final Path path ) { | |
| 57 | assert path != null; | |
| 58 | mDocumentRoot = parse( path ); | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Returns the parent node for the entire YAML document tree. | |
| 63 | * | |
| 64 | * @return The document root, never {@code null}. | |
| 65 | */ | |
| 66 | @Override | |
| 67 | public JsonNode getDocumentRoot() { | |
| 68 | return mDocumentRoot; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Parses the given path containing YAML data into an object hierarchy. | |
| 73 | * | |
| 74 | * @param path {@link Path} to the YAML resource to parse. | |
| 75 | * @return The parsed contents, or an empty object hierarchy. | |
| 76 | */ | |
| 77 | private JsonNode parse( final Path path ) { | |
| 78 | try( final InputStream in = Files.newInputStream( path ) ) { | |
| 79 | return new ObjectMapper( new YAMLFactory() ).readTree( in ); | |
| 80 | } catch( final Exception e ) { | |
| 81 | // Ensure that a document root node exists by relying on the | |
| 82 | // default failure condition when processing. This is required | |
| 83 | // because the input stream could not be read. | |
| 84 | return new ObjectMapper().createObjectNode(); | |
| 85 | } | |
| 86 | } | |
| 87 | } | |
| 1 | 88 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.definition.yaml; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.fasterxml.jackson.databind.node.ObjectNode; | |
| 32 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; | |
| 33 | import com.keenwrite.definition.RootTreeItem; | |
| 34 | import com.keenwrite.definition.TreeAdapter; | |
| 35 | import com.keenwrite.definition.DefinitionTreeItem; | |
| 36 | import javafx.scene.control.TreeItem; | |
| 37 | import javafx.scene.control.TreeView; | |
| 38 | ||
| 39 | import java.io.IOException; | |
| 40 | import java.nio.file.Path; | |
| 41 | import java.util.Map.Entry; | |
| 42 | ||
| 43 | /** | |
| 44 | * Transforms a JsonNode hierarchy into a tree that can be displayed in a user | |
| 45 | * interface and vice-versa. | |
| 46 | */ | |
| 47 | public class YamlTreeAdapter implements TreeAdapter { | |
| 48 | private final YamlParser mParser; | |
| 49 | ||
| 50 | /** | |
| 51 | * Constructs a new instance that will use the given path to read | |
| 52 | * the object hierarchy from a data source. | |
| 53 | * | |
| 54 | * @param path Path to YAML contents to parse. | |
| 55 | */ | |
| 56 | public YamlTreeAdapter( final Path path ) { | |
| 57 | mParser = new YamlParser( path ); | |
| 58 | } | |
| 59 | ||
| 60 | @Override | |
| 61 | public void export( final TreeItem<String> treeItem, final Path path ) | |
| 62 | throws IOException { | |
| 63 | final YAMLMapper mapper = new YAMLMapper(); | |
| 64 | final ObjectNode root = mapper.createObjectNode(); | |
| 65 | ||
| 66 | // Iterate over the root item's children. The root item is used by the | |
| 67 | // application to ensure definitions can always be added to a tree, as | |
| 68 | // such it is not meant to be exported, only its children. | |
| 69 | for( final TreeItem<String> child : treeItem.getChildren() ) { | |
| 70 | export( child, root ); | |
| 71 | } | |
| 72 | ||
| 73 | // Writes as UTF8 by default. | |
| 74 | mapper.writeValue( path.toFile(), root ); | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Recursive method to generate an object hierarchy that represents the | |
| 79 | * given {@link TreeItem} hierarchy. | |
| 80 | * | |
| 81 | * @param item The {@link TreeItem} to reproduce as an object hierarchy. | |
| 82 | * @param node The {@link ObjectNode} to update to reflect the | |
| 83 | * {@link TreeItem} hierarchy. | |
| 84 | */ | |
| 85 | private void export( final TreeItem<String> item, ObjectNode node ) { | |
| 86 | final var children = item.getChildren(); | |
| 87 | ||
| 88 | // If the current item has more than one non-leaf child, it's an | |
| 89 | // object node and must become a new nested object. | |
| 90 | if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) { | |
| 91 | node = node.putObject( item.getValue() ); | |
| 92 | } | |
| 93 | ||
| 94 | for( final TreeItem<String> child : children ) { | |
| 95 | if( child.isLeaf() ) { | |
| 96 | node.put( item.getValue(), child.getValue() ); | |
| 97 | } | |
| 98 | else { | |
| 99 | export( child, node ); | |
| 100 | } | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Converts a YAML document to a {@link TreeItem} based on the document | |
| 106 | * keys. Only the first document in the stream is adapted. | |
| 107 | * | |
| 108 | * @param root Root {@link TreeItem} node name. | |
| 109 | * @return A {@link TreeItem} populated with all the keys in the YAML | |
| 110 | * document. | |
| 111 | */ | |
| 112 | public TreeItem<String> adapt( final String root ) { | |
| 113 | final JsonNode rootNode = getYamlParser().getDocumentRoot(); | |
| 114 | final TreeItem<String> rootItem = createRootTreeItem( root ); | |
| 115 | ||
| 116 | rootItem.setExpanded( true ); | |
| 117 | adapt( rootNode, rootItem ); | |
| 118 | return rootItem; | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Iterate over a given root node (at any level of the tree) and adapt each | |
| 123 | * leaf node. | |
| 124 | * | |
| 125 | * @param rootNode A JSON node (YAML node) to adapt. | |
| 126 | * @param rootItem The tree item to use as the root when processing the node. | |
| 127 | */ | |
| 128 | private void adapt( | |
| 129 | final JsonNode rootNode, final TreeItem<String> rootItem ) { | |
| 130 | rootNode.fields().forEachRemaining( | |
| 131 | ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem ) | |
| 132 | ); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Recursively adapt each rootNode to a corresponding rootItem. | |
| 137 | * | |
| 138 | * @param rootNode The node to adapt. | |
| 139 | * @param rootItem The item to adapt using the node's key. | |
| 140 | */ | |
| 141 | private void adapt( | |
| 142 | final Entry<String, JsonNode> rootNode, | |
| 143 | final TreeItem<String> rootItem ) { | |
| 144 | final JsonNode leafNode = rootNode.getValue(); | |
| 145 | final String key = rootNode.getKey(); | |
| 146 | final TreeItem<String> leaf = createTreeItem( key ); | |
| 147 | ||
| 148 | if( leafNode.isValueNode() ) { | |
| 149 | leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) ); | |
| 150 | } | |
| 151 | ||
| 152 | rootItem.getChildren().add( leaf ); | |
| 153 | ||
| 154 | if( leafNode.isObject() ) { | |
| 155 | adapt( leafNode, leaf ); | |
| 156 | } | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * Creates a new {@link TreeItem} that can be added to the {@link TreeView}. | |
| 161 | * | |
| 162 | * @param value The node's value. | |
| 163 | * @return A new {@link TreeItem}, never {@code null}. | |
| 164 | */ | |
| 165 | private TreeItem<String> createTreeItem( final String value ) { | |
| 166 | return new DefinitionTreeItem<>( value ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Creates a new {@link TreeItem} that is intended to be the root-level item | |
| 171 | * added to the {@link TreeView}. This allows the root item to be | |
| 172 | * distinguished from the other items so that reference keys do not include | |
| 173 | * "Definition" as part of their name. | |
| 174 | * | |
| 175 | * @param value The node's value. | |
| 176 | * @return A new {@link TreeItem}, never {@code null}. | |
| 177 | */ | |
| 178 | private TreeItem<String> createRootTreeItem( final String value ) { | |
| 179 | return new RootTreeItem<>( value ); | |
| 180 | } | |
| 181 | ||
| 182 | public YamlParser getYamlParser() { | |
| 183 | return mParser; | |
| 184 | } | |
| 185 | } | |
| 1 | 186 |
| 1 | /* | |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.dialogs; | |
| 29 | ||
| 30 | import static com.keenwrite.Messages.get; | |
| 31 | import com.keenwrite.service.events.impl.ButtonOrderPane; | |
| 32 | import static javafx.scene.control.ButtonType.CANCEL; | |
| 33 | import static javafx.scene.control.ButtonType.OK; | |
| 34 | import javafx.scene.control.Dialog; | |
| 35 | import javafx.stage.Window; | |
| 36 | ||
| 37 | /** | |
| 38 | * Superclass that abstracts common behaviours for all dialogs. | |
| 39 | * | |
| 40 | * @param <T> The type of dialog to create (usually String). | |
| 41 | */ | |
| 42 | public abstract class AbstractDialog<T> extends Dialog<T> { | |
| 43 | ||
| 44 | /** | |
| 45 | * Ensures that all dialogs can be closed. | |
| 46 | * | |
| 47 | * @param owner The parent window of this dialog. | |
| 48 | * @param title The messages title to display in the title bar. | |
| 49 | */ | |
| 50 | @SuppressWarnings( "OverridableMethodCallInConstructor" ) | |
| 51 | public AbstractDialog( final Window owner, final String title ) { | |
| 52 | setTitle( get( title ) ); | |
| 53 | setResizable( true ); | |
| 54 | ||
| 55 | initOwner( owner ); | |
| 56 | initCloseAction(); | |
| 57 | initDialogPane(); | |
| 58 | initDialogButtons(); | |
| 59 | initComponents(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Initialize the component layout. | |
| 64 | */ | |
| 65 | protected abstract void initComponents(); | |
| 66 | ||
| 67 | /** | |
| 68 | * Set the dialog to use a button order pane with an OK and a CANCEL button. | |
| 69 | */ | |
| 70 | protected void initDialogPane() { | |
| 71 | setDialogPane( new ButtonOrderPane() ); | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Set an OK and CANCEL button on the dialog. | |
| 76 | */ | |
| 77 | protected void initDialogButtons() { | |
| 78 | getDialogPane().getButtonTypes().addAll( OK, CANCEL ); | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Attaches a setOnCloseRequest to the dialog's [X] button so that the user | |
| 83 | * can always close the window, even if there's an error. | |
| 84 | */ | |
| 85 | protected final void initCloseAction() { | |
| 86 | final Window window = getDialogPane().getScene().getWindow(); | |
| 87 | window.setOnCloseRequest( event -> window.hide() ); | |
| 88 | } | |
| 89 | } | |
| 1 | 90 |
| 1 | /* | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.dialogs; | |
| 28 | ||
| 29 | import static com.keenwrite.Messages.get; | |
| 30 | import com.keenwrite.controls.BrowseFileButton; | |
| 31 | import com.keenwrite.controls.EscapeTextField; | |
| 32 | import java.nio.file.Path; | |
| 33 | import javafx.application.Platform; | |
| 34 | import javafx.beans.binding.Bindings; | |
| 35 | import javafx.beans.property.SimpleStringProperty; | |
| 36 | import javafx.beans.property.StringProperty; | |
| 37 | import javafx.scene.control.ButtonBar.ButtonData; | |
| 38 | import static javafx.scene.control.ButtonType.OK; | |
| 39 | import javafx.scene.control.DialogPane; | |
| 40 | import javafx.scene.control.Label; | |
| 41 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 42 | import javafx.stage.Window; | |
| 43 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 44 | ||
| 45 | /** | |
| 46 | * Dialog to enter a markdown image. | |
| 47 | */ | |
| 48 | public class ImageDialog extends AbstractDialog<String> { | |
| 49 | ||
| 50 | private final StringProperty image = new SimpleStringProperty(); | |
| 51 | ||
| 52 | public ImageDialog( final Window owner, final Path basePath ) { | |
| 53 | super(owner, "Dialog.image.title" ); | |
| 54 | ||
| 55 | final DialogPane dialogPane = getDialogPane(); | |
| 56 | dialogPane.setContent( pane ); | |
| 57 | ||
| 58 | linkBrowseFileButton.setBasePath( basePath ); | |
| 59 | linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) ); | |
| 60 | linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() ); | |
| 61 | ||
| 62 | dialogPane.lookupButton( OK ).disableProperty().bind( | |
| 63 | urlField.escapedTextProperty().isEmpty() | |
| 64 | .or( textField.escapedTextProperty().isEmpty() ) ); | |
| 65 | ||
| 66 | image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | |
| 67 | .then( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | |
| 68 | .otherwise( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) ); | |
| 69 | previewField.textProperty().bind( image ); | |
| 70 | ||
| 71 | setResultConverter( dialogButton -> { | |
| 72 | ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null; | |
| 73 | return (data == ButtonData.OK_DONE) ? image.get() : null; | |
| 74 | } ); | |
| 75 | ||
| 76 | Platform.runLater( () -> { | |
| 77 | urlField.requestFocus(); | |
| 78 | ||
| 79 | if( urlField.getText().startsWith( "http://" ) ) { | |
| 80 | urlField.selectRange( "http://".length(), urlField.getLength() ); | |
| 81 | } | |
| 82 | } ); | |
| 83 | } | |
| 84 | ||
| 85 | @Override | |
| 86 | protected void initComponents() { | |
| 87 | // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | |
| 88 | pane = new MigPane(); | |
| 89 | Label urlLabel = new Label(); | |
| 90 | urlField = new EscapeTextField(); | |
| 91 | linkBrowseFileButton = new BrowseFileButton(); | |
| 92 | Label textLabel = new Label(); | |
| 93 | textField = new EscapeTextField(); | |
| 94 | Label titleLabel = new Label(); | |
| 95 | titleField = new EscapeTextField(); | |
| 96 | Label previewLabel = new Label(); | |
| 97 | previewField = new Label(); | |
| 98 | ||
| 99 | //======== pane ======== | |
| 100 | { | |
| 101 | pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" ); | |
| 102 | pane.setRows( "[][][][]" ); | |
| 103 | ||
| 104 | //---- urlLabel ---- | |
| 105 | urlLabel.setText( get( "Dialog.image.urlLabel.text" ) ); | |
| 106 | pane.add( urlLabel, "cell 0 0" ); | |
| 107 | ||
| 108 | //---- urlField ---- | |
| 109 | urlField.setEscapeCharacters( "()" ); | |
| 110 | urlField.setText( "http://yourlink.com" ); | |
| 111 | urlField.setPromptText( "http://yourlink.com" ); | |
| 112 | pane.add( urlField, "cell 1 0" ); | |
| 113 | pane.add( linkBrowseFileButton, "cell 2 0" ); | |
| 114 | ||
| 115 | //---- textLabel ---- | |
| 116 | textLabel.setText( get( "Dialog.image.textLabel.text" ) ); | |
| 117 | pane.add( textLabel, "cell 0 1" ); | |
| 118 | ||
| 119 | //---- textField ---- | |
| 120 | textField.setEscapeCharacters( "[]" ); | |
| 121 | pane.add( textField, "cell 1 1 2 1" ); | |
| 122 | ||
| 123 | //---- titleLabel ---- | |
| 124 | titleLabel.setText( get( "Dialog.image.titleLabel.text" ) ); | |
| 125 | pane.add( titleLabel, "cell 0 2" ); | |
| 126 | pane.add( titleField, "cell 1 2 2 1" ); | |
| 127 | ||
| 128 | //---- previewLabel ---- | |
| 129 | previewLabel.setText( get( "Dialog.image.previewLabel.text" ) ); | |
| 130 | pane.add( previewLabel, "cell 0 3" ); | |
| 131 | pane.add( previewField, "cell 1 3 2 1" ); | |
| 132 | } | |
| 133 | // JFormDesigner - End of component initialization //GEN-END:initComponents | |
| 134 | } | |
| 135 | ||
| 136 | // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | |
| 137 | private MigPane pane; | |
| 138 | private EscapeTextField urlField; | |
| 139 | private BrowseFileButton linkBrowseFileButton; | |
| 140 | private EscapeTextField textField; | |
| 141 | private EscapeTextField titleField; | |
| 142 | private Label previewField; | |
| 143 | // JFormDesigner - End of variables declaration //GEN-END:variables | |
| 144 | } | |
| 1 | 145 |
| 1 | JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | |
| 2 | ||
| 3 | new FormModel { | |
| 4 | "i18n.bundlePackage": "com.scrivendor" | |
| 5 | "i18n.bundleName": "messages" | |
| 6 | "i18n.autoExternalize": true | |
| 7 | "i18n.keyPrefix": "ImageDialog" | |
| 8 | contentType: "form/javafx" | |
| 9 | root: new FormRoot { | |
| 10 | add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | |
| 11 | "$layoutConstraints": "" | |
| 12 | "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]" | |
| 13 | "$rowConstraints": "[][][][]" | |
| 14 | } ) { | |
| 15 | name: "pane" | |
| 16 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 17 | name: "urlLabel" | |
| 18 | "text": new FormMessage( null, "ImageDialog.urlLabel.text" ) | |
| 19 | auxiliary() { | |
| 20 | "JavaCodeGenerator.variableLocal": true | |
| 21 | } | |
| 22 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 23 | "value": "cell 0 0" | |
| 24 | } ) | |
| 25 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 26 | name: "urlField" | |
| 27 | "escapeCharacters": "()" | |
| 28 | "text": "http://yourlink.com" | |
| 29 | "promptText": "http://yourlink.com" | |
| 30 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 31 | "value": "cell 1 0" | |
| 32 | } ) | |
| 33 | add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) { | |
| 34 | name: "linkBrowseFileButton" | |
| 35 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 36 | "value": "cell 2 0" | |
| 37 | } ) | |
| 38 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 39 | name: "textLabel" | |
| 40 | "text": new FormMessage( null, "ImageDialog.textLabel.text" ) | |
| 41 | auxiliary() { | |
| 42 | "JavaCodeGenerator.variableLocal": true | |
| 43 | } | |
| 44 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 45 | "value": "cell 0 1" | |
| 46 | } ) | |
| 47 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 48 | name: "textField" | |
| 49 | "escapeCharacters": "[]" | |
| 50 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 51 | "value": "cell 1 1 2 1" | |
| 52 | } ) | |
| 53 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 54 | name: "titleLabel" | |
| 55 | "text": new FormMessage( null, "ImageDialog.titleLabel.text" ) | |
| 56 | auxiliary() { | |
| 57 | "JavaCodeGenerator.variableLocal": true | |
| 58 | } | |
| 59 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 60 | "value": "cell 0 2" | |
| 61 | } ) | |
| 62 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 63 | name: "titleField" | |
| 64 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 65 | "value": "cell 1 2 2 1" | |
| 66 | } ) | |
| 67 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 68 | name: "previewLabel" | |
| 69 | "text": new FormMessage( null, "ImageDialog.previewLabel.text" ) | |
| 70 | auxiliary() { | |
| 71 | "JavaCodeGenerator.variableLocal": true | |
| 72 | } | |
| 73 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 74 | "value": "cell 0 3" | |
| 75 | } ) | |
| 76 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 77 | name: "previewField" | |
| 78 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 79 | "value": "cell 1 3 2 1" | |
| 80 | } ) | |
| 81 | }, new FormLayoutConstraints( null ) { | |
| 82 | "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | |
| 83 | "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | |
| 84 | } ) | |
| 85 | } | |
| 86 | } | |
| 1 | 87 |
| 1 | /* | |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.dialogs; | |
| 29 | ||
| 30 | import com.keenwrite.controls.EscapeTextField; | |
| 31 | import com.keenwrite.editors.markdown.HyperlinkModel; | |
| 32 | import javafx.application.Platform; | |
| 33 | import javafx.beans.binding.Bindings; | |
| 34 | import javafx.beans.property.SimpleStringProperty; | |
| 35 | import javafx.beans.property.StringProperty; | |
| 36 | import javafx.scene.control.ButtonBar.ButtonData; | |
| 37 | import javafx.scene.control.DialogPane; | |
| 38 | import javafx.scene.control.Label; | |
| 39 | import javafx.stage.Window; | |
| 40 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 41 | ||
| 42 | import static com.keenwrite.Messages.get; | |
| 43 | import static javafx.scene.control.ButtonType.OK; | |
| 44 | ||
| 45 | /** | |
| 46 | * Dialog to enter a markdown link. | |
| 47 | */ | |
| 48 | public class LinkDialog extends AbstractDialog<String> { | |
| 49 | ||
| 50 | private final StringProperty link = new SimpleStringProperty(); | |
| 51 | ||
| 52 | public LinkDialog( | |
| 53 | final Window owner, final HyperlinkModel hyperlink ) { | |
| 54 | super( owner, "Dialog.link.title" ); | |
| 55 | ||
| 56 | final DialogPane dialogPane = getDialogPane(); | |
| 57 | dialogPane.setContent( pane ); | |
| 58 | ||
| 59 | dialogPane.lookupButton( OK ).disableProperty().bind( | |
| 60 | urlField.escapedTextProperty().isEmpty() ); | |
| 61 | ||
| 62 | textField.setText( hyperlink.getText() ); | |
| 63 | urlField.setText( hyperlink.getUrl() ); | |
| 64 | titleField.setText( hyperlink.getTitle() ); | |
| 65 | ||
| 66 | link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | |
| 67 | .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | |
| 68 | .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() ) | |
| 69 | .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) | |
| 70 | .otherwise( urlField.escapedTextProperty() ) ) ); | |
| 71 | ||
| 72 | setResultConverter( dialogButton -> { | |
| 73 | ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null; | |
| 74 | return (data == ButtonData.OK_DONE) ? link.get() : null; | |
| 75 | } ); | |
| 76 | ||
| 77 | Platform.runLater( () -> { | |
| 78 | urlField.requestFocus(); | |
| 79 | urlField.selectRange( 0, urlField.getLength() ); | |
| 80 | } ); | |
| 81 | } | |
| 82 | ||
| 83 | @Override | |
| 84 | protected void initComponents() { | |
| 85 | // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | |
| 86 | pane = new MigPane(); | |
| 87 | Label urlLabel = new Label(); | |
| 88 | urlField = new EscapeTextField(); | |
| 89 | Label textLabel = new Label(); | |
| 90 | textField = new EscapeTextField(); | |
| 91 | Label titleLabel = new Label(); | |
| 92 | titleField = new EscapeTextField(); | |
| 93 | ||
| 94 | //======== pane ======== | |
| 95 | { | |
| 96 | pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" ); | |
| 97 | pane.setRows( "[][][][]" ); | |
| 98 | ||
| 99 | //---- urlLabel ---- | |
| 100 | urlLabel.setText( get( "Dialog.link.urlLabel.text" ) ); | |
| 101 | pane.add( urlLabel, "cell 0 0" ); | |
| 102 | ||
| 103 | //---- urlField ---- | |
| 104 | urlField.setEscapeCharacters( "()" ); | |
| 105 | pane.add( urlField, "cell 1 0" ); | |
| 106 | ||
| 107 | //---- textLabel ---- | |
| 108 | textLabel.setText( get( "Dialog.link.textLabel.text" ) ); | |
| 109 | pane.add( textLabel, "cell 0 1" ); | |
| 110 | ||
| 111 | //---- textField ---- | |
| 112 | textField.setEscapeCharacters( "[]" ); | |
| 113 | pane.add( textField, "cell 1 1 3 1" ); | |
| 114 | ||
| 115 | //---- titleLabel ---- | |
| 116 | titleLabel.setText( get( "Dialog.link.titleLabel.text" ) ); | |
| 117 | pane.add( titleLabel, "cell 0 2" ); | |
| 118 | pane.add( titleField, "cell 1 2 3 1" ); | |
| 119 | } | |
| 120 | // JFormDesigner - End of component initialization //GEN-END:initComponents | |
| 121 | } | |
| 122 | ||
| 123 | // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | |
| 124 | private MigPane pane; | |
| 125 | private EscapeTextField urlField; | |
| 126 | private EscapeTextField textField; | |
| 127 | private EscapeTextField titleField; | |
| 128 | // JFormDesigner - End of variables declaration //GEN-END:variables | |
| 129 | } | |
| 1 | 130 |
| 1 | JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | |
| 2 | ||
| 3 | new FormModel { | |
| 4 | "i18n.bundlePackage": "com.scrivendor" | |
| 5 | "i18n.bundleName": "messages" | |
| 6 | "i18n.autoExternalize": true | |
| 7 | "i18n.keyPrefix": "LinkDialog" | |
| 8 | contentType: "form/javafx" | |
| 9 | root: new FormRoot { | |
| 10 | add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | |
| 11 | "$layoutConstraints": "" | |
| 12 | "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]" | |
| 13 | "$rowConstraints": "[][][][]" | |
| 14 | } ) { | |
| 15 | name: "pane" | |
| 16 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 17 | name: "urlLabel" | |
| 18 | "text": new FormMessage( null, "LinkDialog.urlLabel.text" ) | |
| 19 | auxiliary() { | |
| 20 | "JavaCodeGenerator.variableLocal": true | |
| 21 | } | |
| 22 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 23 | "value": "cell 0 0" | |
| 24 | } ) | |
| 25 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 26 | name: "urlField" | |
| 27 | "escapeCharacters": "()" | |
| 28 | "text": "http://yourlink.com" | |
| 29 | "promptText": "http://yourlink.com" | |
| 30 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 31 | "value": "cell 1 0" | |
| 32 | } ) | |
| 33 | add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) { | |
| 34 | name: "linkBrowseDirectoyButton" | |
| 35 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 36 | "value": "cell 2 0" | |
| 37 | } ) | |
| 38 | add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) { | |
| 39 | name: "linkBrowseFileButton" | |
| 40 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 41 | "value": "cell 3 0" | |
| 42 | } ) | |
| 43 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 44 | name: "textLabel" | |
| 45 | "text": new FormMessage( null, "LinkDialog.textLabel.text" ) | |
| 46 | auxiliary() { | |
| 47 | "JavaCodeGenerator.variableLocal": true | |
| 48 | } | |
| 49 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 50 | "value": "cell 0 1" | |
| 51 | } ) | |
| 52 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 53 | name: "textField" | |
| 54 | "escapeCharacters": "[]" | |
| 55 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 56 | "value": "cell 1 1 3 1" | |
| 57 | } ) | |
| 58 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 59 | name: "titleLabel" | |
| 60 | "text": new FormMessage( null, "LinkDialog.titleLabel.text" ) | |
| 61 | auxiliary() { | |
| 62 | "JavaCodeGenerator.variableLocal": true | |
| 63 | } | |
| 64 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 65 | "value": "cell 0 2" | |
| 66 | } ) | |
| 67 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 68 | name: "titleField" | |
| 69 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 70 | "value": "cell 1 2 3 1" | |
| 71 | } ) | |
| 72 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 73 | name: "previewLabel" | |
| 74 | "text": new FormMessage( null, "LinkDialog.previewLabel.text" ) | |
| 75 | auxiliary() { | |
| 76 | "JavaCodeGenerator.variableLocal": true | |
| 77 | } | |
| 78 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 79 | "value": "cell 0 3" | |
| 80 | } ) | |
| 81 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 82 | name: "previewField" | |
| 83 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 84 | "value": "cell 1 3 3 1" | |
| 85 | } ) | |
| 86 | }, new FormLayoutConstraints( null ) { | |
| 87 | "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | |
| 88 | "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | |
| 89 | } ) | |
| 90 | } | |
| 91 | } | |
| 1 | 92 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors; | |
| 29 | ||
| 30 | import com.keenwrite.AbstractFileFactory; | |
| 31 | import com.keenwrite.sigils.RSigilOperator; | |
| 32 | import com.keenwrite.sigils.SigilOperator; | |
| 33 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 34 | ||
| 35 | import java.nio.file.Path; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for creating a definition name decorator suited to a particular | |
| 39 | * file type. | |
| 40 | */ | |
| 41 | public class DefinitionDecoratorFactory extends AbstractFileFactory { | |
| 42 | ||
| 43 | private DefinitionDecoratorFactory() { | |
| 44 | } | |
| 45 | ||
| 46 | public static SigilOperator newInstance( final Path path ) { | |
| 47 | final var factory = new DefinitionDecoratorFactory(); | |
| 48 | ||
| 49 | return switch( factory.lookup( path ) ) { | |
| 50 | case RMARKDOWN, RXML -> new RSigilOperator(); | |
| 51 | default -> new YamlSigilOperator(); | |
| 52 | }; | |
| 53 | } | |
| 54 | } | |
| 1 | 55 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors; | |
| 29 | ||
| 30 | import com.keenwrite.FileEditorTab; | |
| 31 | import com.keenwrite.definition.DefinitionPane; | |
| 32 | import com.keenwrite.definition.DefinitionTreeItem; | |
| 33 | import com.keenwrite.sigils.SigilOperator; | |
| 34 | import javafx.scene.control.TreeItem; | |
| 35 | import javafx.scene.input.KeyEvent; | |
| 36 | import org.fxmisc.richtext.StyledTextArea; | |
| 37 | ||
| 38 | import java.nio.file.Path; | |
| 39 | import java.text.BreakIterator; | |
| 40 | ||
| 41 | import static com.keenwrite.Constants.*; | |
| 42 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 43 | import static java.lang.Character.isWhitespace; | |
| 44 | import static javafx.scene.input.KeyCode.SPACE; | |
| 45 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 46 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 47 | ||
| 48 | /** | |
| 49 | * Provides the logic for injecting variable names within the editor. | |
| 50 | */ | |
| 51 | public final class DefinitionNameInjector { | |
| 52 | ||
| 53 | /** | |
| 54 | * Recipient of name injections. | |
| 55 | */ | |
| 56 | private FileEditorTab mTab; | |
| 57 | ||
| 58 | /** | |
| 59 | * Initiates double-click events. | |
| 60 | */ | |
| 61 | private final DefinitionPane mDefinitionPane; | |
| 62 | ||
| 63 | /** | |
| 64 | * Initializes the variable name injector against the given pane. | |
| 65 | * | |
| 66 | * @param pane The definition panel to listen to for double-click events. | |
| 67 | */ | |
| 68 | public DefinitionNameInjector( final DefinitionPane pane ) { | |
| 69 | mDefinitionPane = pane; | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Trap Control+Space. | |
| 74 | * | |
| 75 | * @param tab Editor where variable names get injected. | |
| 76 | */ | |
| 77 | public void addListener( final FileEditorTab tab ) { | |
| 78 | assert tab != null; | |
| 79 | mTab = tab; | |
| 80 | ||
| 81 | tab.getEditorPane().addKeyboardListener( | |
| 82 | keyPressed( SPACE, CONTROL_DOWN ), | |
| 83 | this::autoinsert | |
| 84 | ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Inserts the currently selected variable from the {@link DefinitionPane}. | |
| 89 | */ | |
| 90 | public void injectSelectedItem() { | |
| 91 | final var pane = getDefinitionPane(); | |
| 92 | final TreeItem<String> item = pane.getSelectedItem(); | |
| 93 | ||
| 94 | if( item.isLeaf() ) { | |
| 95 | final var leaf = pane.findLeafExact( item.getValue() ); | |
| 96 | final var editor = getEditor(); | |
| 97 | ||
| 98 | editor.insertText( editor.getCaretPosition(), decorate( leaf ) ); | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | /** | |
| 103 | * Pressing Control+SPACE will find a node that matches the current word and | |
| 104 | * substitute the definition reference. | |
| 105 | */ | |
| 106 | public void autoinsert() { | |
| 107 | final String paragraph = getCaretParagraph(); | |
| 108 | final int[] bounds = getWordBoundariesAtCaret(); | |
| 109 | ||
| 110 | try { | |
| 111 | if( isEmptyDefinitionPane() ) { | |
| 112 | alert( STATUS_DEFINITION_EMPTY ); | |
| 113 | } | |
| 114 | else { | |
| 115 | final String word = paragraph.substring( bounds[ 0 ], bounds[ 1 ] ); | |
| 116 | ||
| 117 | if( word.isBlank() ) { | |
| 118 | alert( STATUS_DEFINITION_BLANK ); | |
| 119 | } | |
| 120 | else { | |
| 121 | final var leaf = findLeaf( word ); | |
| 122 | ||
| 123 | if( leaf == null ) { | |
| 124 | alert( STATUS_DEFINITION_MISSING, word ); | |
| 125 | } | |
| 126 | else { | |
| 127 | replaceText( bounds[ 0 ], bounds[ 1 ], decorate( leaf ) ); | |
| 128 | expand( leaf ); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | } catch( final Exception ignored ) { | |
| 133 | alert( STATUS_DEFINITION_BLANK ); | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | /** | |
| 138 | * Pressing Control+SPACE will find a node that matches the current word and | |
| 139 | * substitute the definition reference. | |
| 140 | * | |
| 141 | * @param e Ignored -- it can only be Control+SPACE. | |
| 142 | */ | |
| 143 | private void autoinsert( final KeyEvent e ) { | |
| 144 | autoinsert(); | |
| 145 | } | |
| 146 | ||
| 147 | /** | |
| 148 | * Finds the start and end indexes for the word in the current paragraph | |
| 149 | * where the caret is located. There are a few different scenarios, where | |
| 150 | * the caret can be at: the start, end, or middle of a word; also, the | |
| 151 | * caret can be at the end or beginning of a punctuated word; as well, the | |
| 152 | * caret could be at the beginning or end of the line or document. | |
| 153 | */ | |
| 154 | private int[] getWordBoundariesAtCaret() { | |
| 155 | final var paragraph = getCaretParagraph(); | |
| 156 | final var length = paragraph.length(); | |
| 157 | int offset = getCurrentCaretColumn(); | |
| 158 | ||
| 159 | int began = offset; | |
| 160 | int ended = offset; | |
| 161 | ||
| 162 | while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) { | |
| 163 | began--; | |
| 164 | } | |
| 165 | ||
| 166 | while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) { | |
| 167 | ended++; | |
| 168 | } | |
| 169 | ||
| 170 | final var iterator = BreakIterator.getWordInstance(); | |
| 171 | iterator.setText( paragraph ); | |
| 172 | ||
| 173 | while( began < length && iterator.isBoundary( began + 1 ) ) { | |
| 174 | began++; | |
| 175 | } | |
| 176 | ||
| 177 | while( ended > 0 && iterator.isBoundary( ended - 1 ) ) { | |
| 178 | ended--; | |
| 179 | } | |
| 180 | ||
| 181 | return new int[]{began, ended}; | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Decorates a {@link TreeItem} using the syntax specific to the type of | |
| 186 | * document being edited. | |
| 187 | * | |
| 188 | * @param leaf The path to the leaf (the definition key) to be decorated. | |
| 189 | */ | |
| 190 | private String decorate( final DefinitionTreeItem<String> leaf ) { | |
| 191 | return decorate( leaf.toPath() ); | |
| 192 | } | |
| 193 | ||
| 194 | /** | |
| 195 | * Decorates a variable using the syntax specific to the type of document | |
| 196 | * being edited. | |
| 197 | * | |
| 198 | * @param variable The variable to decorate in dot-notation without any | |
| 199 | * start or end sigils present. | |
| 200 | */ | |
| 201 | private String decorate( final String variable ) { | |
| 202 | return getVariableDecorator().apply( variable ); | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * Updates the text at the given position within the current paragraph. | |
| 207 | * | |
| 208 | * @param posBegan The starting index in the paragraph text to replace. | |
| 209 | * @param posEnded The ending index in the paragraph text to replace. | |
| 210 | * @param text Overwrite the paragraph substring with this text. | |
| 211 | */ | |
| 212 | private void replaceText( | |
| 213 | final int posBegan, final int posEnded, final String text ) { | |
| 214 | final int p = getCurrentParagraph(); | |
| 215 | ||
| 216 | getEditor().replaceText( p, posBegan, p, posEnded, text ); | |
| 217 | } | |
| 218 | ||
| 219 | /** | |
| 220 | * Returns the caret's current paragraph position. | |
| 221 | * | |
| 222 | * @return A number greater than or equal to 0. | |
| 223 | */ | |
| 224 | private int getCurrentParagraph() { | |
| 225 | return getEditor().getCurrentParagraph(); | |
| 226 | } | |
| 227 | ||
| 228 | /** | |
| 229 | * Returns the text for the paragraph that contains the caret. | |
| 230 | * | |
| 231 | * @return A non-null string, possibly empty. | |
| 232 | */ | |
| 233 | private String getCaretParagraph() { | |
| 234 | return getEditor().getText( getCurrentParagraph() ); | |
| 235 | } | |
| 236 | ||
| 237 | /** | |
| 238 | * Returns the caret position within the current paragraph. | |
| 239 | * | |
| 240 | * @return A value from 0 to the length of the current paragraph. | |
| 241 | */ | |
| 242 | private int getCurrentCaretColumn() { | |
| 243 | return getEditor().getCaretColumn(); | |
| 244 | } | |
| 245 | ||
| 246 | /** | |
| 247 | * Looks for the given word, matching first by exact, next by a starts-with | |
| 248 | * condition with diacritics replaced, then by containment. | |
| 249 | * | |
| 250 | * @param word The word to match by: exact, at the beginning, or containment. | |
| 251 | * @return The matching {@link DefinitionTreeItem} for the given word, or | |
| 252 | * {@code null} if none found. | |
| 253 | */ | |
| 254 | @SuppressWarnings("ConstantConditions") | |
| 255 | private DefinitionTreeItem<String> findLeaf( final String word ) { | |
| 256 | assert word != null; | |
| 257 | ||
| 258 | final var pane = getDefinitionPane(); | |
| 259 | DefinitionTreeItem<String> leaf = null; | |
| 260 | ||
| 261 | leaf = leaf == null ? pane.findLeafExact( word ) : leaf; | |
| 262 | leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf; | |
| 263 | leaf = leaf == null ? pane.findLeafContains( word ) : leaf; | |
| 264 | leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf; | |
| 265 | ||
| 266 | return leaf; | |
| 267 | } | |
| 268 | ||
| 269 | /** | |
| 270 | * Answers whether there are any definitions in the tree. | |
| 271 | * | |
| 272 | * @return {@code true} when there are no definitions; {@code false} when | |
| 273 | * there's at least one definition. | |
| 274 | */ | |
| 275 | private boolean isEmptyDefinitionPane() { | |
| 276 | return getDefinitionPane().isEmpty(); | |
| 277 | } | |
| 278 | ||
| 279 | /** | |
| 280 | * Collapses the tree then expands and selects the given node. | |
| 281 | * | |
| 282 | * @param node The node to expand. | |
| 283 | */ | |
| 284 | private void expand( final TreeItem<String> node ) { | |
| 285 | final DefinitionPane pane = getDefinitionPane(); | |
| 286 | pane.collapse(); | |
| 287 | pane.expand( node ); | |
| 288 | pane.select( node ); | |
| 289 | } | |
| 290 | ||
| 291 | /** | |
| 292 | * @return A variable decorator that corresponds to the given file type. | |
| 293 | */ | |
| 294 | private SigilOperator getVariableDecorator() { | |
| 295 | return DefinitionDecoratorFactory.newInstance( getFilename() ); | |
| 296 | } | |
| 297 | ||
| 298 | private Path getFilename() { | |
| 299 | return getFileEditorTab().getPath(); | |
| 300 | } | |
| 301 | ||
| 302 | private EditorPane getEditorPane() { | |
| 303 | return getFileEditorTab().getEditorPane(); | |
| 304 | } | |
| 305 | ||
| 306 | private StyledTextArea<?, ?> getEditor() { | |
| 307 | return getEditorPane().getEditor(); | |
| 308 | } | |
| 309 | ||
| 310 | public FileEditorTab getFileEditorTab() { | |
| 311 | return mTab; | |
| 312 | } | |
| 313 | ||
| 314 | private DefinitionPane getDefinitionPane() { | |
| 315 | return mDefinitionPane; | |
| 316 | } | |
| 317 | } | |
| 1 | 318 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors; | |
| 29 | ||
| 30 | import com.keenwrite.preferences.UserPreferences; | |
| 31 | import javafx.beans.property.IntegerProperty; | |
| 32 | import javafx.beans.property.ObjectProperty; | |
| 33 | import javafx.beans.property.SimpleObjectProperty; | |
| 34 | import javafx.beans.value.ChangeListener; | |
| 35 | import javafx.event.Event; | |
| 36 | import javafx.scene.control.ScrollPane; | |
| 37 | import javafx.scene.layout.Pane; | |
| 38 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 39 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 40 | import org.fxmisc.undo.UndoManager; | |
| 41 | import org.fxmisc.wellbehaved.event.EventPattern; | |
| 42 | import org.fxmisc.wellbehaved.event.Nodes; | |
| 43 | ||
| 44 | import java.nio.file.Path; | |
| 45 | import java.util.function.Consumer; | |
| 46 | ||
| 47 | import static com.keenwrite.StatusBarNotifier.clearAlert; | |
| 48 | import static java.lang.String.format; | |
| 49 | import static javafx.application.Platform.runLater; | |
| 50 | import static org.fxmisc.wellbehaved.event.InputMap.consume; | |
| 51 | ||
| 52 | /** | |
| 53 | * Represents common editing features for various types of text editors. | |
| 54 | */ | |
| 55 | public class EditorPane extends Pane { | |
| 56 | ||
| 57 | /** | |
| 58 | * Used when changing the text area font size. | |
| 59 | */ | |
| 60 | private static final String FMT_CSS_FONT_SIZE = "-fx-font-size: %dpt;"; | |
| 61 | ||
| 62 | private final StyleClassedTextArea mEditor = | |
| 63 | new StyleClassedTextArea( false ); | |
| 64 | private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane = | |
| 65 | new VirtualizedScrollPane<>( mEditor ); | |
| 66 | private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>(); | |
| 67 | ||
| 68 | public EditorPane() { | |
| 69 | getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS ); | |
| 70 | fontsSizeProperty().addListener( | |
| 71 | ( l, o, n ) -> setFontSize( n.intValue() ) | |
| 72 | ); | |
| 73 | ||
| 74 | // Clear out any previous alerts after the user has typed. If the problem | |
| 75 | // persists, re-rendering the document will re-raise the error. If there | |
| 76 | // was no previous error, clearing the alert is essentially a no-op. | |
| 77 | mEditor.textProperty().addListener( | |
| 78 | ( l, o, n ) -> clearAlert() | |
| 79 | ); | |
| 80 | } | |
| 81 | ||
| 82 | @Override | |
| 83 | public void requestFocus() { | |
| 84 | requestFocus( 3 ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * There's a race-condition between displaying the {@link EditorPane} | |
| 89 | * and giving the {@link #mEditor} focus. Try to focus up to {@code max} | |
| 90 | * times before giving up. | |
| 91 | * | |
| 92 | * @param max The number of attempts to try to request focus. | |
| 93 | */ | |
| 94 | private void requestFocus( final int max ) { | |
| 95 | if( max > 0 ) { | |
| 96 | runLater( | |
| 97 | () -> { | |
| 98 | final var editor = getEditor(); | |
| 99 | ||
| 100 | if( !editor.isFocused() ) { | |
| 101 | editor.requestFocus(); | |
| 102 | requestFocus( max - 1 ); | |
| 103 | } | |
| 104 | } | |
| 105 | ); | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | public void undo() { | |
| 110 | getUndoManager().undo(); | |
| 111 | } | |
| 112 | ||
| 113 | public void redo() { | |
| 114 | getUndoManager().redo(); | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * Cuts the actively selected text; if no text is selected, this will cut | |
| 119 | * the entire paragraph. | |
| 120 | */ | |
| 121 | public void cut() { | |
| 122 | final var editor = getEditor(); | |
| 123 | final var selected = editor.getSelectedText(); | |
| 124 | ||
| 125 | if( selected == null || selected.isEmpty() ) { | |
| 126 | editor.selectParagraph(); | |
| 127 | } | |
| 128 | ||
| 129 | editor.cut(); | |
| 130 | } | |
| 131 | ||
| 132 | public void copy() { | |
| 133 | getEditor().copy(); | |
| 134 | } | |
| 135 | ||
| 136 | public void paste() { | |
| 137 | getEditor().paste(); | |
| 138 | } | |
| 139 | ||
| 140 | public void selectAll() { | |
| 141 | getEditor().selectAll(); | |
| 142 | } | |
| 143 | ||
| 144 | public UndoManager<?> getUndoManager() { | |
| 145 | return getEditor().getUndoManager(); | |
| 146 | } | |
| 147 | ||
| 148 | public String getText() { | |
| 149 | return getEditor().getText(); | |
| 150 | } | |
| 151 | ||
| 152 | public void setText( final String text ) { | |
| 153 | final var editor = getEditor(); | |
| 154 | editor.deselect(); | |
| 155 | editor.replaceText( text ); | |
| 156 | getUndoManager().mark(); | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * Call to hook into changes to the text area. | |
| 161 | * | |
| 162 | * @param listener Receives editor text change events. | |
| 163 | */ | |
| 164 | public void addTextChangeListener( | |
| 165 | final ChangeListener<? super String> listener ) { | |
| 166 | getEditor().textProperty().addListener( listener ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Notifies observers when the caret changes paragraph. | |
| 171 | * | |
| 172 | * @param listener Receives change event. | |
| 173 | */ | |
| 174 | public void addCaretParagraphListener( | |
| 175 | final ChangeListener<? super Integer> listener ) { | |
| 176 | getEditor().currentParagraphProperty().addListener( listener ); | |
| 177 | } | |
| 178 | ||
| 179 | /** | |
| 180 | * Notifies observers when the caret changes position. | |
| 181 | * | |
| 182 | * @param listener Receives change event. | |
| 183 | */ | |
| 184 | public void addCaretPositionListener( | |
| 185 | final ChangeListener<? super Integer> listener ) { | |
| 186 | getEditor().caretPositionProperty().addListener( listener ); | |
| 187 | } | |
| 188 | ||
| 189 | /** | |
| 190 | * This method adds listeners to editor events. | |
| 191 | * | |
| 192 | * @param <T> The event type. | |
| 193 | * @param <U> The consumer type for the given event type. | |
| 194 | * @param event The event of interest. | |
| 195 | * @param consumer The method to call when the event happens. | |
| 196 | */ | |
| 197 | public <T extends Event, U extends T> void addKeyboardListener( | |
| 198 | final EventPattern<? super T, ? extends U> event, | |
| 199 | final Consumer<? super U> consumer ) { | |
| 200 | Nodes.addInputMap( getEditor(), consume( event, consumer ) ); | |
| 201 | } | |
| 202 | ||
| 203 | /** | |
| 204 | * Repositions the cursor and scroll bar to the top of the file. | |
| 205 | */ | |
| 206 | public void scrollToTop() { | |
| 207 | getEditor().moveTo( 0 ); | |
| 208 | getScrollPane().scrollYToPixel( 0 ); | |
| 209 | } | |
| 210 | ||
| 211 | public StyleClassedTextArea getEditor() { | |
| 212 | return mEditor; | |
| 213 | } | |
| 214 | ||
| 215 | /** | |
| 216 | * Returns the scroll pane that contains the text area. | |
| 217 | * | |
| 218 | * @return The scroll pane that contains the content to edit. | |
| 219 | */ | |
| 220 | public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { | |
| 221 | return mScrollPane; | |
| 222 | } | |
| 223 | ||
| 224 | public Path getPath() { | |
| 225 | return mPath.get(); | |
| 226 | } | |
| 227 | ||
| 228 | public void setPath( final Path path ) { | |
| 229 | mPath.set( path ); | |
| 230 | } | |
| 231 | ||
| 232 | /** | |
| 233 | * Sets the font size in points. | |
| 234 | * | |
| 235 | * @param size The new font size to use for the text editor. | |
| 236 | */ | |
| 237 | private void setFontSize( final int size ) { | |
| 238 | mEditor.setStyle( format( FMT_CSS_FONT_SIZE, size ) ); | |
| 239 | } | |
| 240 | ||
| 241 | /** | |
| 242 | * Returns the text editor font size property for handling font size change | |
| 243 | * events. | |
| 244 | */ | |
| 245 | private IntegerProperty fontsSizeProperty() { | |
| 246 | return UserPreferences.getInstance().fontsSizeEditorProperty(); | |
| 247 | } | |
| 248 | } | |
| 1 | 249 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors.markdown; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.ast.Link; | |
| 31 | ||
| 32 | /** | |
| 33 | * Represents the model for a hyperlink: text, url, and title. | |
| 34 | */ | |
| 35 | public class HyperlinkModel { | |
| 36 | ||
| 37 | private String text; | |
| 38 | private String url; | |
| 39 | private String title; | |
| 40 | ||
| 41 | /** | |
| 42 | * Constructs a new hyperlink model in Markdown format by default with no | |
| 43 | * title (i.e., tooltip). | |
| 44 | * | |
| 45 | * @param text The hyperlink text displayed (e.g., displayed to the user). | |
| 46 | * @param url The destination URL (e.g., when clicked). | |
| 47 | */ | |
| 48 | public HyperlinkModel( final String text, final String url ) { | |
| 49 | this( text, url, null ); | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Constructs a new hyperlink model for the given AST link. | |
| 54 | * | |
| 55 | * @param link A markdown link. | |
| 56 | */ | |
| 57 | public HyperlinkModel( final Link link ) { | |
| 58 | this( | |
| 59 | link.getText().toString(), | |
| 60 | link.getUrl().toString(), | |
| 61 | link.getTitle().toString() | |
| 62 | ); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Constructs a new hyperlink model in Markdown format by default. | |
| 67 | * | |
| 68 | * @param text The hyperlink text displayed (e.g., displayed to the user). | |
| 69 | * @param url The destination URL (e.g., when clicked). | |
| 70 | * @param title The hyperlink title (e.g., shown as a tooltip). | |
| 71 | */ | |
| 72 | public HyperlinkModel( final String text, final String url, | |
| 73 | final String title ) { | |
| 74 | setText( text ); | |
| 75 | setUrl( url ); | |
| 76 | setTitle( title ); | |
| 77 | } | |
| 78 | ||
| 79 | /** | |
| 80 | * Returns the string in Markdown format by default. | |
| 81 | * | |
| 82 | * @return A markdown version of the hyperlink. | |
| 83 | */ | |
| 84 | @Override | |
| 85 | public String toString() { | |
| 86 | String format = "%s%s%s"; | |
| 87 | ||
| 88 | if( hasText() ) { | |
| 89 | format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)"); | |
| 90 | } | |
| 91 | ||
| 92 | // Becomes ""+URL+"" if no text is set. | |
| 93 | // Becomes [TITLE]+(URL)+"" if no title is set. | |
| 94 | // Becomes [TITLE]+(URL+ \"TITLE\") if title is set. | |
| 95 | return String.format( format, getText(), getUrl(), getTitle() ); | |
| 96 | } | |
| 97 | ||
| 98 | public final void setText( final String text ) { | |
| 99 | this.text = nullSafe( text ); | |
| 100 | } | |
| 101 | ||
| 102 | public final void setUrl( final String url ) { | |
| 103 | this.url = nullSafe( url ); | |
| 104 | } | |
| 105 | ||
| 106 | public final void setTitle( final String title ) { | |
| 107 | this.title = nullSafe( title ); | |
| 108 | } | |
| 109 | ||
| 110 | /** | |
| 111 | * Answers whether text has been set for the hyperlink. | |
| 112 | * | |
| 113 | * @return true This is a text link. | |
| 114 | */ | |
| 115 | public boolean hasText() { | |
| 116 | return !getText().isEmpty(); | |
| 117 | } | |
| 118 | ||
| 119 | /** | |
| 120 | * Answers whether a title (tooltip) has been set for the hyperlink. | |
| 121 | * | |
| 122 | * @return true There is a title. | |
| 123 | */ | |
| 124 | public boolean hasTitle() { | |
| 125 | return !getTitle().isEmpty(); | |
| 126 | } | |
| 127 | ||
| 128 | public String getText() { | |
| 129 | return this.text; | |
| 130 | } | |
| 131 | ||
| 132 | public String getUrl() { | |
| 133 | return this.url; | |
| 134 | } | |
| 135 | ||
| 136 | public String getTitle() { | |
| 137 | return this.title; | |
| 138 | } | |
| 139 | ||
| 140 | private String nullSafe( final String s ) { | |
| 141 | return s == null ? "" : s; | |
| 142 | } | |
| 143 | } | |
| 1 | 144 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors.markdown; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.ast.Link; | |
| 31 | import com.vladsch.flexmark.util.ast.Node; | |
| 32 | import com.vladsch.flexmark.util.ast.NodeVisitor; | |
| 33 | import com.vladsch.flexmark.util.ast.VisitHandler; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for extracting a hyperlink from the document so that the user | |
| 37 | * can edit the link within a dialog. | |
| 38 | */ | |
| 39 | public class LinkVisitor { | |
| 40 | ||
| 41 | private NodeVisitor visitor; | |
| 42 | private Link link; | |
| 43 | private final int offset; | |
| 44 | ||
| 45 | /** | |
| 46 | * Creates a hyperlink given an offset into a paragraph and the markdown AST | |
| 47 | * link node. | |
| 48 | * | |
| 49 | * @param index Index into the paragraph that indicates the hyperlink to | |
| 50 | * change. | |
| 51 | */ | |
| 52 | public LinkVisitor( final int index ) { | |
| 53 | this.offset = index; | |
| 54 | } | |
| 55 | ||
| 56 | public Link process( final Node root ) { | |
| 57 | getVisitor().visit( root ); | |
| 58 | return getLink(); | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * @param link Not null. | |
| 63 | */ | |
| 64 | private void visit( final Link link ) { | |
| 65 | final int began = link.getStartOffset(); | |
| 66 | final int ended = link.getEndOffset(); | |
| 67 | final int index = getOffset(); | |
| 68 | ||
| 69 | if( index >= began && index <= ended ) { | |
| 70 | setLink( link ); | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | private synchronized NodeVisitor getVisitor() { | |
| 75 | if( this.visitor == null ) { | |
| 76 | this.visitor = createVisitor(); | |
| 77 | } | |
| 78 | ||
| 79 | return this.visitor; | |
| 80 | } | |
| 81 | ||
| 82 | protected NodeVisitor createVisitor() { | |
| 83 | return new NodeVisitor( | |
| 84 | new VisitHandler<>( Link.class, LinkVisitor.this::visit ) ); | |
| 85 | } | |
| 86 | ||
| 87 | private Link getLink() { | |
| 88 | return this.link; | |
| 89 | } | |
| 90 | ||
| 91 | private void setLink( final Link link ) { | |
| 92 | this.link = link; | |
| 93 | } | |
| 94 | ||
| 95 | public int getOffset() { | |
| 96 | return this.offset; | |
| 97 | } | |
| 98 | } | |
| 1 | 99 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.editors.markdown; | |
| 29 | ||
| 30 | import com.keenwrite.dialogs.ImageDialog; | |
| 31 | import com.keenwrite.dialogs.LinkDialog; | |
| 32 | import com.keenwrite.editors.EditorPane; | |
| 33 | import com.keenwrite.processors.markdown.BlockExtension; | |
| 34 | import com.keenwrite.processors.markdown.MarkdownProcessor; | |
| 35 | import com.vladsch.flexmark.ast.Link; | |
| 36 | import com.vladsch.flexmark.html.renderer.AttributablePart; | |
| 37 | import com.vladsch.flexmark.util.ast.Node; | |
| 38 | import com.vladsch.flexmark.util.html.MutableAttributes; | |
| 39 | import javafx.scene.control.Dialog; | |
| 40 | import javafx.scene.control.IndexRange; | |
| 41 | import javafx.scene.input.KeyCode; | |
| 42 | import javafx.scene.input.KeyEvent; | |
| 43 | import javafx.stage.Window; | |
| 44 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 45 | ||
| 46 | import java.nio.file.Path; | |
| 47 | import java.util.ArrayList; | |
| 48 | import java.util.List; | |
| 49 | import java.util.regex.Matcher; | |
| 50 | import java.util.regex.Pattern; | |
| 51 | ||
| 52 | import static com.keenwrite.Constants.STYLESHEET_MARKDOWN; | |
| 53 | import static com.keenwrite.util.Utils.ltrim; | |
| 54 | import static com.keenwrite.util.Utils.rtrim; | |
| 55 | import static javafx.scene.input.KeyCode.ENTER; | |
| 56 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 57 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 58 | ||
| 59 | /** | |
| 60 | * Provides the ability to edit a text document. | |
| 61 | */ | |
| 62 | public class MarkdownEditorPane extends EditorPane { | |
| 63 | private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile( | |
| 64 | "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" ); | |
| 65 | ||
| 66 | /** | |
| 67 | * Any of these followed by a space and a letter produce a line | |
| 68 | * by themselves. The ">" need not be followed by a space. | |
| 69 | */ | |
| 70 | private static final Pattern PATTERN_NEW_LINE = Pattern.compile( | |
| 71 | "^>|(((#+)|([*+\\-])|([1-9]\\.))\\s+).+" ); | |
| 72 | ||
| 73 | public MarkdownEditorPane() { | |
| 74 | initEditor(); | |
| 75 | } | |
| 76 | ||
| 77 | private void initEditor() { | |
| 78 | final StyleClassedTextArea textArea = getEditor(); | |
| 79 | ||
| 80 | textArea.setWrapText( true ); | |
| 81 | textArea.getStyleClass().add( "markdown-editor" ); | |
| 82 | textArea.getStylesheets().add( STYLESHEET_MARKDOWN ); | |
| 83 | ||
| 84 | addKeyboardListener( keyPressed( ENTER ), this::enterPressed ); | |
| 85 | addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut ); | |
| 86 | } | |
| 87 | ||
| 88 | public void insertLink() { | |
| 89 | insertObject( createLinkDialog() ); | |
| 90 | } | |
| 91 | ||
| 92 | public void insertImage() { | |
| 93 | insertObject( createImageDialog() ); | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Returns the editor's paragraph number that will be close to its HTML | |
| 98 | * paragraph ID. Ultimately this solution is flawed because there isn't | |
| 99 | * a straightforward correlation between the document being edited and | |
| 100 | * what is rendered. XML documents transformed through stylesheets have | |
| 101 | * no readily determined correlation. Images, tables, and other | |
| 102 | * objects affect the relative location of the current paragraph being | |
| 103 | * edited with respect to the preview pane. | |
| 104 | * <p> | |
| 105 | * See | |
| 106 | * {@link BlockExtension.IdAttributeProvider#setAttributes(Node, AttributablePart, MutableAttributes)}} | |
| 107 | * for details. | |
| 108 | * </p> | |
| 109 | * <p> | |
| 110 | * Injecting a token into the document, as per a previous version of the | |
| 111 | * application, can instruct the preview pane where to shift the viewport. | |
| 112 | * </p> | |
| 113 | * | |
| 114 | * @param paraIndex The paragraph index from the editor pane to scroll to | |
| 115 | * in the preview pane, which will be approximated if an | |
| 116 | * equivalent cannot be found. | |
| 117 | * @return A unique identifier that correlates to an equivalent paragraph | |
| 118 | * number once the Markdown is rendered into HTML. | |
| 119 | */ | |
| 120 | public int approximateParagraphId( final int paraIndex ) { | |
| 121 | final StyleClassedTextArea editor = getEditor(); | |
| 122 | final List<String> lines = new ArrayList<>( 4096 ); | |
| 123 | ||
| 124 | int i = 0; | |
| 125 | String prevText = ""; | |
| 126 | boolean withinFencedBlock = false; | |
| 127 | boolean withinCodeBlock = false; | |
| 128 | ||
| 129 | for( final var p : editor.getParagraphs() ) { | |
| 130 | if( i > paraIndex ) { | |
| 131 | break; | |
| 132 | } | |
| 133 | ||
| 134 | final String text = p.getText().replace( '>', ' ' ); | |
| 135 | if( text.startsWith( "```" ) ) { | |
| 136 | if( withinFencedBlock = !withinFencedBlock ) { | |
| 137 | lines.add( text ); | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | if( !withinFencedBlock ) { | |
| 142 | final boolean foundCodeBlock = text.startsWith( " " ); | |
| 143 | ||
| 144 | if( foundCodeBlock && !withinCodeBlock ) { | |
| 145 | lines.add( text ); | |
| 146 | withinCodeBlock = true; | |
| 147 | } | |
| 148 | else if( !foundCodeBlock ) { | |
| 149 | withinCodeBlock = false; | |
| 150 | } | |
| 151 | } | |
| 152 | ||
| 153 | if( !withinFencedBlock && !withinCodeBlock && | |
| 154 | ((!text.isBlank() && prevText.isBlank()) || | |
| 155 | PATTERN_NEW_LINE.matcher( text ).matches()) ) { | |
| 156 | lines.add( text ); | |
| 157 | } | |
| 158 | ||
| 159 | prevText = text; | |
| 160 | i++; | |
| 161 | } | |
| 162 | ||
| 163 | // Scrolling index is 1-based. | |
| 164 | return Math.max( lines.size() - 1, 0 ); | |
| 165 | } | |
| 166 | ||
| 167 | /** | |
| 168 | * Gets the index of the paragraph where the caret is positioned. | |
| 169 | * | |
| 170 | * @return The paragraph number for the caret. | |
| 171 | */ | |
| 172 | public int getCurrentParagraphIndex() { | |
| 173 | return getEditor().getCurrentParagraph(); | |
| 174 | } | |
| 175 | ||
| 176 | /** | |
| 177 | * @param leading Characters to insert at the beginning of the current | |
| 178 | * selection (or paragraph). | |
| 179 | * @param trailing Characters to insert at the end of the current selection | |
| 180 | * (or paragraph). | |
| 181 | */ | |
| 182 | public void surroundSelection( final String leading, final String trailing ) { | |
| 183 | surroundSelection( leading, trailing, null ); | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * @param leading Characters to insert at the beginning of the current | |
| 188 | * selection (or paragraph). | |
| 189 | * @param trailing Characters to insert at the end of the current selection | |
| 190 | * (or paragraph). | |
| 191 | * @param hint Instructional text inserted within the leading and | |
| 192 | * trailing characters, provided no text is selected. | |
| 193 | */ | |
| 194 | public void surroundSelection( | |
| 195 | String leading, String trailing, final String hint ) { | |
| 196 | final StyleClassedTextArea textArea = getEditor(); | |
| 197 | ||
| 198 | // Note: not using textArea.insertText() to insert leading and trailing | |
| 199 | // because this would add two changes to undo history | |
| 200 | final IndexRange selection = textArea.getSelection(); | |
| 201 | int start = selection.getStart(); | |
| 202 | int end = selection.getEnd(); | |
| 203 | ||
| 204 | final String selectedText = textArea.getSelectedText(); | |
| 205 | ||
| 206 | String trimmedText = selectedText.trim(); | |
| 207 | if( trimmedText.length() < selectedText.length() ) { | |
| 208 | start += selectedText.indexOf( trimmedText ); | |
| 209 | end = start + trimmedText.length(); | |
| 210 | } | |
| 211 | ||
| 212 | // remove leading whitespaces from leading text if selection starts at zero | |
| 213 | if( start == 0 ) { | |
| 214 | leading = ltrim( leading ); | |
| 215 | } | |
| 216 | ||
| 217 | // remove trailing whitespaces from trailing text if selection ends at | |
| 218 | // text end | |
| 219 | if( end == textArea.getLength() ) { | |
| 220 | trailing = rtrim( trailing ); | |
| 221 | } | |
| 222 | ||
| 223 | // remove leading line separators from leading text | |
| 224 | // if there are line separators before the selected text | |
| 225 | if( leading.startsWith( "\n" ) ) { | |
| 226 | for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) { | |
| 227 | if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) { | |
| 228 | break; | |
| 229 | } | |
| 230 | ||
| 231 | leading = leading.substring( 1 ); | |
| 232 | } | |
| 233 | } | |
| 234 | ||
| 235 | // remove trailing line separators from trailing or leading text | |
| 236 | // if there are line separators after the selected text | |
| 237 | final boolean trailingIsEmpty = trailing.isEmpty(); | |
| 238 | String str = trailingIsEmpty ? leading : trailing; | |
| 239 | ||
| 240 | if( str.endsWith( "\n" ) ) { | |
| 241 | final int length = textArea.getLength(); | |
| 242 | ||
| 243 | for( int i = end; i < length && str.endsWith( "\n" ); i++ ) { | |
| 244 | if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) { | |
| 245 | break; | |
| 246 | } | |
| 247 | ||
| 248 | str = str.substring( 0, str.length() - 1 ); | |
| 249 | } | |
| 250 | ||
| 251 | if( trailingIsEmpty ) { | |
| 252 | leading = str; | |
| 253 | } | |
| 254 | else { | |
| 255 | trailing = str; | |
| 256 | } | |
| 257 | } | |
| 258 | ||
| 259 | int selStart = start + leading.length(); | |
| 260 | int selEnd = end + leading.length(); | |
| 261 | ||
| 262 | // insert hint text if selection is empty | |
| 263 | if( hint != null && trimmedText.isEmpty() ) { | |
| 264 | trimmedText = hint; | |
| 265 | selEnd = selStart + hint.length(); | |
| 266 | } | |
| 267 | ||
| 268 | // prevent undo merging with previous text entered by user | |
| 269 | getUndoManager().preventMerge(); | |
| 270 | ||
| 271 | // replace text and update selection | |
| 272 | textArea.replaceText( start, end, leading + trimmedText + trailing ); | |
| 273 | textArea.selectRange( selStart, selEnd ); | |
| 274 | } | |
| 275 | ||
| 276 | private void enterPressed( final KeyEvent e ) { | |
| 277 | final StyleClassedTextArea textArea = getEditor(); | |
| 278 | final String currentLine = | |
| 279 | textArea.getText( textArea.getCurrentParagraph() ); | |
| 280 | final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine ); | |
| 281 | ||
| 282 | String newText = "\n"; | |
| 283 | ||
| 284 | if( matcher.matches() ) { | |
| 285 | if( !matcher.group( 2 ).isEmpty() ) { | |
| 286 | // indent new line with same whitespace characters and list markers | |
| 287 | // as current line | |
| 288 | newText = newText.concat( matcher.group( 1 ) ); | |
| 289 | } | |
| 290 | else { | |
| 291 | // current line contains only whitespace characters and list markers | |
| 292 | // --> empty current line | |
| 293 | final int caretPosition = textArea.getCaretPosition(); | |
| 294 | textArea.selectRange( caretPosition - currentLine.length(), | |
| 295 | caretPosition ); | |
| 296 | } | |
| 297 | } | |
| 298 | ||
| 299 | textArea.replaceSelection( newText ); | |
| 300 | ||
| 301 | // Ensure that the window scrolls when Enter is pressed at the bottom of | |
| 302 | // the pane. | |
| 303 | textArea.requestFollowCaret(); | |
| 304 | } | |
| 305 | ||
| 306 | private void cut( final KeyEvent event ) { | |
| 307 | super.cut(); | |
| 308 | } | |
| 309 | ||
| 310 | /** | |
| 311 | * Returns one of: selected text, word under cursor, or parsed hyperlink from | |
| 312 | * the markdown AST. | |
| 313 | * | |
| 314 | * @return An instance containing the link URL and display text. | |
| 315 | */ | |
| 316 | private HyperlinkModel getHyperlink() { | |
| 317 | final StyleClassedTextArea textArea = getEditor(); | |
| 318 | final String selectedText = textArea.getSelectedText(); | |
| 319 | ||
| 320 | // Get the current paragraph, convert to Markdown nodes. | |
| 321 | final MarkdownProcessor mp = new MarkdownProcessor( null ); | |
| 322 | final int p = textArea.getCurrentParagraph(); | |
| 323 | final String paragraph = textArea.getText( p ); | |
| 324 | final Node node = mp.toNode( paragraph ); | |
| 325 | final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() ); | |
| 326 | final Link link = visitor.process( node ); | |
| 327 | ||
| 328 | if( link != null ) { | |
| 329 | textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() ); | |
| 330 | } | |
| 331 | ||
| 332 | return createHyperlinkModel( | |
| 333 | link, selectedText, "https://localhost" | |
| 334 | ); | |
| 335 | } | |
| 336 | ||
| 337 | @SuppressWarnings("SameParameterValue") | |
| 338 | private HyperlinkModel createHyperlinkModel( | |
| 339 | final Link link, final String selection, final String url ) { | |
| 340 | ||
| 341 | return link == null | |
| 342 | ? new HyperlinkModel( selection, url ) | |
| 343 | : new HyperlinkModel( link ); | |
| 344 | } | |
| 345 | ||
| 346 | private Path getParentPath() { | |
| 347 | final Path path = getPath(); | |
| 348 | return (path != null) ? path.getParent() : null; | |
| 349 | } | |
| 350 | ||
| 351 | private Dialog<String> createLinkDialog() { | |
| 352 | return new LinkDialog( getWindow(), getHyperlink() ); | |
| 353 | } | |
| 354 | ||
| 355 | private Dialog<String> createImageDialog() { | |
| 356 | return new ImageDialog( getWindow(), getParentPath() ); | |
| 357 | } | |
| 358 | ||
| 359 | private void insertObject( final Dialog<String> dialog ) { | |
| 360 | dialog.showAndWait().ifPresent( | |
| 361 | result -> getEditor().replaceSelection( result ) | |
| 362 | ); | |
| 363 | } | |
| 364 | ||
| 365 | private Window getWindow() { | |
| 366 | return getScrollPane().getScene().getWindow(); | |
| 367 | } | |
| 368 | } | |
| 1 | 369 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.exceptions; | |
| 29 | ||
| 30 | import java.io.FileNotFoundException; | |
| 31 | ||
| 32 | import static com.keenwrite.Messages.get; | |
| 33 | ||
| 34 | /** | |
| 35 | * Responsible for informing the user when a file cannot be found. | |
| 36 | * This avoids duplicating the error message prefix. | |
| 37 | */ | |
| 38 | public class MissingFileException extends FileNotFoundException { | |
| 39 | /** | |
| 40 | * Constructs a new {@link MissingFileException} using the given path. | |
| 41 | * | |
| 42 | * @param uri The path to the file resource that could not be found. | |
| 43 | */ | |
| 44 | public MissingFileException( final String uri ) { | |
| 45 | super( get( "Main.status.error.file.missing", uri ) ); | |
| 46 | } | |
| 47 | } | |
| 1 | 48 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.predicates; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.util.Collection; | |
| 32 | import java.util.function.Predicate; | |
| 33 | ||
| 34 | import static java.lang.String.join; | |
| 35 | import static java.nio.file.FileSystems.getDefault; | |
| 36 | ||
| 37 | /** | |
| 38 | * Provides a number of simple {@link Predicate} instances for various types | |
| 39 | * of string comparisons, including basic strings and file name strings. | |
| 40 | */ | |
| 41 | public class PredicateFactory { | |
| 42 | /** | |
| 43 | * Creates an instance of {@link Predicate} that matches a globbed file | |
| 44 | * name pattern. | |
| 45 | * | |
| 46 | * @param pattern The file name pattern to match. | |
| 47 | * @return A {@link Predicate} that can answer whether a given file name | |
| 48 | * matches the given glob pattern. | |
| 49 | */ | |
| 50 | public static Predicate<File> createFileTypePredicate( | |
| 51 | final String pattern ) { | |
| 52 | final var matcher = getDefault().getPathMatcher( | |
| 53 | "glob:**{" + pattern + "}" | |
| 54 | ); | |
| 55 | ||
| 56 | return file -> matcher.matches( file.toPath() ); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Creates an instance of {@link Predicate} that matches any file name from | |
| 61 | * a {@link Collection} of file name patterns. The given patterns are joined | |
| 62 | * with commas into a single comma-separated list. | |
| 63 | * | |
| 64 | * @param patterns The file name patterns to be matched. | |
| 65 | * @return A {@link Predicate} that can answer whether a given file name | |
| 66 | * matches the given glob patterns. | |
| 67 | */ | |
| 68 | public static Predicate<File> createFileTypePredicate( | |
| 69 | final Collection<String> patterns ) { | |
| 70 | return createFileTypePredicate( join( ",", patterns ) ); | |
| 71 | } | |
| 72 | ||
| 73 | /** | |
| 74 | * Creates an instance of {@link Predicate} that compares whether the given | |
| 75 | * {@code reference} string is contained by the comparator. Comparison is | |
| 76 | * case-insensitive. The test will also pass if the comparate is empty. | |
| 77 | * | |
| 78 | * @param comparator The string to check as being contained. | |
| 79 | * @return A {@link Predicate} that can answer whether the given string | |
| 80 | * is contained within the comparator, or the comparate is empty. | |
| 81 | */ | |
| 82 | public static Predicate<String> createStringContainsPredicate( | |
| 83 | final String comparator ) { | |
| 84 | return comparate -> comparate.isEmpty() || | |
| 85 | comparate.toLowerCase().contains( comparator.toLowerCase() ); | |
| 86 | } | |
| 1 | 87 | |
| 88 | /** | |
| 89 | * Creates an instance of {@link Predicate} that compares whether the given | |
| 90 | * {@code reference} string is starts with the comparator. Comparison is | |
| 91 | * case-insensitive. | |
| 92 | * | |
| 93 | * @param comparator The string to check as being contained. | |
| 94 | * @return A {@link Predicate} that can answer whether the given string | |
| 95 | * is contained within the comparator. | |
| 96 | */ | |
| 97 | public static Predicate<String> createStringStartsPredicate( | |
| 98 | final String comparator ) { | |
| 99 | return comparate -> | |
| 100 | comparate.toLowerCase().startsWith( comparator.toLowerCase() ); | |
| 101 | } | |
| 102 | } |
| 1 | /* | |
| 2 | * Copyright 2016 David Croft and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preferences; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.io.FileInputStream; | |
| 32 | import java.io.FileOutputStream; | |
| 33 | import java.util.*; | |
| 34 | import java.util.prefs.AbstractPreferences; | |
| 35 | import java.util.prefs.BackingStoreException; | |
| 36 | ||
| 37 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 38 | ||
| 39 | /** | |
| 40 | * Preferences implementation that stores to a user-defined file. Local file | |
| 41 | * storage is preferred over a certain operating system's monolithic trash heap | |
| 42 | * called a registry. When the OS is locked down, the default Preferences | |
| 43 | * implementation will try to write to the registry and fail due to permissions | |
| 44 | * problems. This class sidesteps the issue entirely by writing to the user's | |
| 45 | * home directory, where permissions should be a bit more lax. | |
| 46 | */ | |
| 47 | public class FilePreferences extends AbstractPreferences { | |
| 48 | ||
| 49 | private final Map<String, String> mRoot = new TreeMap<>(); | |
| 50 | private final Map<String, FilePreferences> mChildren = new TreeMap<>(); | |
| 51 | private boolean mRemoved; | |
| 52 | ||
| 53 | private final Object mMutex = new Object(); | |
| 54 | ||
| 55 | public FilePreferences( | |
| 56 | final AbstractPreferences parent, final String name ) { | |
| 57 | super( parent, name ); | |
| 58 | ||
| 59 | try { | |
| 60 | sync(); | |
| 61 | } catch( final BackingStoreException ex ) { | |
| 62 | alert( ex ); | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | @Override | |
| 67 | protected void putSpi( final String key, final String value ) { | |
| 68 | synchronized( mMutex ) { | |
| 69 | mRoot.put( key, value ); | |
| 70 | } | |
| 71 | ||
| 72 | try { | |
| 73 | flush(); | |
| 74 | } catch( final BackingStoreException ex ) { | |
| 75 | alert( ex ); | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | @Override | |
| 80 | protected String getSpi( final String key ) { | |
| 81 | synchronized( mMutex ) { | |
| 82 | return mRoot.get( key ); | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | @Override | |
| 87 | protected void removeSpi( final String key ) { | |
| 88 | synchronized( mMutex ) { | |
| 89 | mRoot.remove( key ); | |
| 90 | } | |
| 91 | ||
| 92 | try { | |
| 93 | flush(); | |
| 94 | } catch( final BackingStoreException ex ) { | |
| 95 | alert( ex ); | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | @Override | |
| 100 | protected void removeNodeSpi() throws BackingStoreException { | |
| 101 | mRemoved = true; | |
| 102 | flush(); | |
| 103 | } | |
| 104 | ||
| 105 | @Override | |
| 106 | protected String[] keysSpi() { | |
| 107 | synchronized( mMutex ) { | |
| 108 | return mRoot.keySet().toArray( new String[ 0 ] ); | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | @Override | |
| 113 | protected String[] childrenNamesSpi() { | |
| 114 | return mChildren.keySet().toArray( new String[ 0 ] ); | |
| 115 | } | |
| 116 | ||
| 117 | @Override | |
| 118 | protected FilePreferences childSpi( final String name ) { | |
| 119 | FilePreferences child = mChildren.get( name ); | |
| 120 | ||
| 121 | if( child == null || child.isRemoved() ) { | |
| 122 | child = new FilePreferences( this, name ); | |
| 123 | mChildren.put( name, child ); | |
| 124 | } | |
| 125 | ||
| 126 | return child; | |
| 127 | } | |
| 128 | ||
| 129 | @Override | |
| 130 | protected void syncSpi() { | |
| 131 | if( isRemoved() ) { | |
| 132 | return; | |
| 133 | } | |
| 134 | ||
| 135 | final File file = FilePreferencesFactory.getPreferencesFile(); | |
| 136 | ||
| 137 | if( !file.exists() ) { | |
| 138 | return; | |
| 139 | } | |
| 140 | ||
| 141 | synchronized( mMutex ) { | |
| 142 | final Properties p = new Properties(); | |
| 143 | ||
| 144 | try( final var inputStream = new FileInputStream( file ) ) { | |
| 145 | p.load( inputStream ); | |
| 146 | ||
| 147 | final String path = getPath(); | |
| 148 | final Enumeration<?> propertyNames = p.propertyNames(); | |
| 149 | ||
| 150 | while( propertyNames.hasMoreElements() ) { | |
| 151 | final String propKey = (String) propertyNames.nextElement(); | |
| 152 | ||
| 153 | if( propKey.startsWith( path ) ) { | |
| 154 | final String subKey = propKey.substring( path.length() ); | |
| 155 | ||
| 156 | // Only load immediate descendants | |
| 157 | if( subKey.indexOf( '.' ) == -1 ) { | |
| 158 | mRoot.put( subKey, p.getProperty( propKey ) ); | |
| 159 | } | |
| 160 | } | |
| 161 | } | |
| 162 | } catch( final Exception ex ) { | |
| 163 | alert( ex ); | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | private String getPath() { | |
| 169 | final FilePreferences parent = (FilePreferences) parent(); | |
| 170 | ||
| 171 | return parent == null ? "" : parent.getPath() + name() + '.'; | |
| 172 | } | |
| 173 | ||
| 174 | @Override | |
| 175 | protected void flushSpi() { | |
| 176 | final File file = FilePreferencesFactory.getPreferencesFile(); | |
| 177 | ||
| 178 | synchronized( mMutex ) { | |
| 179 | final Properties p = new Properties(); | |
| 180 | ||
| 181 | try { | |
| 182 | final String path = getPath(); | |
| 183 | ||
| 184 | if( file.exists() ) { | |
| 185 | try( final var fis = new FileInputStream( file ) ) { | |
| 186 | p.load( fis ); | |
| 187 | } | |
| 188 | ||
| 189 | final List<String> toRemove = new ArrayList<>(); | |
| 190 | ||
| 191 | // Make a list of all direct children of this node to be removed | |
| 192 | final Enumeration<?> propertyNames = p.propertyNames(); | |
| 193 | ||
| 194 | while( propertyNames.hasMoreElements() ) { | |
| 195 | final String propKey = (String) propertyNames.nextElement(); | |
| 196 | if( propKey.startsWith( path ) ) { | |
| 197 | final String subKey = propKey.substring( path.length() ); | |
| 198 | ||
| 199 | // Only do immediate descendants | |
| 200 | if( subKey.indexOf( '.' ) == -1 ) { | |
| 201 | toRemove.add( propKey ); | |
| 202 | } | |
| 203 | } | |
| 204 | } | |
| 205 | ||
| 206 | // Remove them now that the enumeration is done with | |
| 207 | for( final String propKey : toRemove ) { | |
| 208 | p.remove( propKey ); | |
| 209 | } | |
| 210 | } | |
| 211 | ||
| 212 | // If this node hasn't been removed, add back in any values | |
| 213 | if( !mRemoved ) { | |
| 214 | for( final String s : mRoot.keySet() ) { | |
| 215 | p.setProperty( path + s, mRoot.get( s ) ); | |
| 216 | } | |
| 217 | } | |
| 218 | ||
| 219 | try( final var fos = new FileOutputStream( file ) ) { | |
| 220 | p.store( fos, "FilePreferences" ); | |
| 221 | } | |
| 222 | } catch( final Exception ex ) { | |
| 223 | alert( ex ); | |
| 224 | } | |
| 225 | } | |
| 226 | } | |
| 227 | } | |
| 1 | 228 |
| 1 | /* | |
| 2 | * Copyright 2016 David Croft and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preferences; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.util.prefs.Preferences; | |
| 32 | import java.util.prefs.PreferencesFactory; | |
| 33 | ||
| 34 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | |
| 35 | import static java.io.File.separator; | |
| 36 | import static java.lang.System.getProperty; | |
| 37 | ||
| 38 | /** | |
| 39 | * PreferencesFactory implementation that stores the preferences in a | |
| 40 | * user-defined file. Usage: | |
| 41 | * <pre> | |
| 42 | * System.setProperty( "java.util.prefs.PreferencesFactory", | |
| 43 | * FilePreferencesFactory.class.getName() ); | |
| 44 | * </pre> | |
| 45 | */ | |
| 46 | public class FilePreferencesFactory implements PreferencesFactory { | |
| 47 | ||
| 48 | private static File sPreferencesFile; | |
| 49 | private Preferences rootPreferences; | |
| 50 | ||
| 51 | @Override | |
| 52 | public Preferences systemRoot() { | |
| 53 | return userRoot(); | |
| 54 | } | |
| 55 | ||
| 56 | @Override | |
| 57 | public Preferences userRoot() { | |
| 58 | final var prefs = rootPreferences; | |
| 59 | ||
| 60 | if( prefs == null ) { | |
| 61 | rootPreferences = new FilePreferences( null, "" ); | |
| 62 | } | |
| 63 | ||
| 64 | return rootPreferences; | |
| 65 | } | |
| 66 | ||
| 67 | public static File getPreferencesFile() { | |
| 68 | final var prefs = sPreferencesFile; | |
| 69 | ||
| 70 | if( prefs == null ) { | |
| 71 | sPreferencesFile = new File( getPreferencesFilename() ).getAbsoluteFile(); | |
| 72 | } | |
| 73 | ||
| 74 | return sPreferencesFile; | |
| 75 | } | |
| 76 | ||
| 77 | public static String getPreferencesFilename() { | |
| 78 | final var filename = getProperty( "application.name", APP_TITLE_LOWERCASE ); | |
| 79 | return getProperty( "user.home" ) + separator + '.' + filename; | |
| 80 | } | |
| 81 | } | |
| 1 | 82 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preferences; | |
| 29 | ||
| 30 | import com.dlsc.formsfx.model.structure.StringField; | |
| 31 | import com.dlsc.preferencesfx.PreferencesFx; | |
| 32 | import com.dlsc.preferencesfx.PreferencesFxEvent; | |
| 33 | import com.dlsc.preferencesfx.model.Category; | |
| 34 | import com.dlsc.preferencesfx.model.Group; | |
| 35 | import com.dlsc.preferencesfx.model.Setting; | |
| 36 | import javafx.beans.property.*; | |
| 37 | import javafx.event.EventHandler; | |
| 38 | import javafx.scene.Node; | |
| 39 | import javafx.scene.control.Label; | |
| 40 | ||
| 41 | import java.io.File; | |
| 42 | import java.nio.file.Path; | |
| 43 | ||
| 44 | import static com.keenwrite.Constants.*; | |
| 45 | import static com.keenwrite.Messages.get; | |
| 46 | ||
| 47 | /** | |
| 48 | * Responsible for user preferences that can be changed from the GUI. The | |
| 49 | * settings are displayed and persisted using {@link PreferencesFx}. | |
| 50 | */ | |
| 51 | public class UserPreferences { | |
| 52 | /** | |
| 53 | * Implementation of the initialization-on-demand holder design pattern, | |
| 54 | * an for a lazy-loaded singleton. In all versions of Java, the idiom enables | |
| 55 | * a safe, highly concurrent lazy initialization of static fields with good | |
| 56 | * performance. The implementation relies upon the initialization phase of | |
| 57 | * execution within the Java Virtual Machine (JVM) as specified by the Java | |
| 58 | * Language Specification. When the class {@link UserPreferencesContainer} | |
| 59 | * is loaded, its initialization completes trivially because there are no | |
| 60 | * static variables to initialize. | |
| 61 | * <p> | |
| 62 | * The static class definition {@link UserPreferencesContainer} within the | |
| 63 | * {@link UserPreferences} is not initialized until such time that | |
| 64 | * {@link UserPreferencesContainer} must be executed. The static | |
| 65 | * {@link UserPreferencesContainer} class executes when | |
| 66 | * {@link #getInstance} is called. The first call will trigger loading and | |
| 67 | * initialization of the {@link UserPreferencesContainer} thereby | |
| 68 | * instantiating the {@link #INSTANCE}. | |
| 69 | * </p> | |
| 70 | * <p> | |
| 71 | * This indirection is necessary because the {@link UserPreferences} class | |
| 72 | * references {@link PreferencesFx}, which must not be instantiated until the | |
| 73 | * UI is ready. | |
| 74 | * </p> | |
| 75 | */ | |
| 76 | private static class UserPreferencesContainer { | |
| 77 | private static final UserPreferences INSTANCE = new UserPreferences(); | |
| 78 | } | |
| 79 | ||
| 80 | public static UserPreferences getInstance() { | |
| 81 | return UserPreferencesContainer.INSTANCE; | |
| 82 | } | |
| 83 | ||
| 84 | private final PreferencesFx mPreferencesFx; | |
| 85 | ||
| 86 | private final ObjectProperty<File> mPropRDirectory; | |
| 87 | private final StringProperty mPropRScript; | |
| 88 | private final ObjectProperty<File> mPropImagesDirectory; | |
| 89 | private final StringProperty mPropImagesOrder; | |
| 90 | private final ObjectProperty<File> mPropDefinitionPath; | |
| 91 | private final StringProperty mRDelimiterBegan; | |
| 92 | private final StringProperty mRDelimiterEnded; | |
| 93 | private final StringProperty mDefDelimiterBegan; | |
| 94 | private final StringProperty mDefDelimiterEnded; | |
| 95 | private final IntegerProperty mPropFontsSizeEditor; | |
| 96 | ||
| 97 | private UserPreferences() { | |
| 98 | mPropRDirectory = simpleFile( USER_DIRECTORY ); | |
| 99 | mPropRScript = new SimpleStringProperty( "" ); | |
| 100 | ||
| 101 | mPropImagesDirectory = simpleFile( USER_DIRECTORY ); | |
| 102 | mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ); | |
| 103 | ||
| 104 | mPropDefinitionPath = simpleFile( | |
| 105 | getSetting( "file.definition.default", DEFINITION_NAME ) | |
| 106 | ); | |
| 107 | ||
| 108 | mDefDelimiterBegan = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ); | |
| 109 | mDefDelimiterEnded = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ); | |
| 110 | ||
| 111 | mRDelimiterBegan = new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ); | |
| 112 | mRDelimiterEnded = new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ); | |
| 113 | ||
| 114 | mPropFontsSizeEditor = new SimpleIntegerProperty( (int) FONT_SIZE_EDITOR ); | |
| 115 | ||
| 116 | // All properties must be initialized before creating the dialog. | |
| 117 | mPreferencesFx = createPreferencesFx(); | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Display the user preferences settings dialog (non-modal). | |
| 122 | */ | |
| 123 | public void show() { | |
| 124 | getPreferencesFx().show( false ); | |
| 125 | } | |
| 126 | ||
| 127 | /** | |
| 128 | * Call to persist the settings. Strictly speaking, this could watch on | |
| 129 | * all values for external changes then save automatically. | |
| 130 | */ | |
| 131 | public void save() { | |
| 132 | getPreferencesFx().saveSettings(); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Creates the preferences dialog. | |
| 137 | * <p> | |
| 138 | * TODO: Make this dynamic by iterating over all "Preferences.*" values | |
| 139 | * that follow a particular naming pattern. | |
| 140 | * </p> | |
| 141 | * | |
| 142 | * @return A new instance of preferences for users to edit. | |
| 143 | */ | |
| 144 | @SuppressWarnings("unchecked") | |
| 145 | private PreferencesFx createPreferencesFx() { | |
| 146 | final Setting<StringField, StringProperty> scriptSetting = | |
| 147 | Setting.of( "Script", mPropRScript ); | |
| 148 | final StringField field = scriptSetting.getElement(); | |
| 149 | field.multiline( true ); | |
| 150 | ||
| 151 | return PreferencesFx.of( | |
| 152 | UserPreferences.class, | |
| 153 | Category.of( | |
| 154 | get( "Preferences.r" ), | |
| 155 | Group.of( | |
| 156 | get( "Preferences.r.directory" ), | |
| 157 | Setting.of( label( "Preferences.r.directory.desc", false ) ), | |
| 158 | Setting.of( "Directory", mPropRDirectory, true ) | |
| 159 | ), | |
| 160 | Group.of( | |
| 161 | get( "Preferences.r.script" ), | |
| 162 | Setting.of( label( "Preferences.r.script.desc" ) ), | |
| 163 | scriptSetting | |
| 164 | ), | |
| 165 | Group.of( | |
| 166 | get( "Preferences.r.delimiter.began" ), | |
| 167 | Setting.of( label( "Preferences.r.delimiter.began.desc" ) ), | |
| 168 | Setting.of( "Opening", mRDelimiterBegan ) | |
| 169 | ), | |
| 170 | Group.of( | |
| 171 | get( "Preferences.r.delimiter.ended" ), | |
| 172 | Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ), | |
| 173 | Setting.of( "Closing", mRDelimiterEnded ) | |
| 174 | ) | |
| 175 | ), | |
| 176 | Category.of( | |
| 177 | get( "Preferences.images" ), | |
| 178 | Group.of( | |
| 179 | get( "Preferences.images.directory" ), | |
| 180 | Setting.of( label( "Preferences.images.directory.desc" ) ), | |
| 181 | Setting.of( "Directory", mPropImagesDirectory, true ) | |
| 182 | ), | |
| 183 | Group.of( | |
| 184 | get( "Preferences.images.suffixes" ), | |
| 185 | Setting.of( label( "Preferences.images.suffixes.desc" ) ), | |
| 186 | Setting.of( "Extensions", mPropImagesOrder ) | |
| 187 | ) | |
| 188 | ), | |
| 189 | Category.of( | |
| 190 | get( "Preferences.definitions" ), | |
| 191 | Group.of( | |
| 192 | get( "Preferences.definitions.path" ), | |
| 193 | Setting.of( label( "Preferences.definitions.path.desc" ) ), | |
| 194 | Setting.of( "Path", mPropDefinitionPath, false ) | |
| 195 | ), | |
| 196 | Group.of( | |
| 197 | get( "Preferences.definitions.delimiter.began" ), | |
| 198 | Setting.of( label( | |
| 199 | "Preferences.definitions.delimiter.began.desc" ) ), | |
| 200 | Setting.of( "Opening", mDefDelimiterBegan ) | |
| 201 | ), | |
| 202 | Group.of( | |
| 203 | get( "Preferences.definitions.delimiter.ended" ), | |
| 204 | Setting.of( label( | |
| 205 | "Preferences.definitions.delimiter.ended.desc" ) ), | |
| 206 | Setting.of( "Closing", mDefDelimiterEnded ) | |
| 207 | ) | |
| 208 | ), | |
| 209 | Category.of( | |
| 210 | get( "Preferences.fonts" ), | |
| 211 | Group.of( | |
| 212 | get( "Preferences.fonts.size_editor" ), | |
| 213 | Setting.of( label( "Preferences.fonts.size_editor.desc" ) ), | |
| 214 | Setting.of( "Points", mPropFontsSizeEditor ) | |
| 215 | ) | |
| 216 | ) | |
| 217 | ).instantPersistent( false ) | |
| 218 | .dialogIcon( ICON_DIALOG ); | |
| 219 | } | |
| 220 | ||
| 221 | /** | |
| 222 | * Wraps a {@link File} inside a {@link SimpleObjectProperty}. | |
| 223 | * | |
| 224 | * @param path The file name to use when constructing the {@link File}. | |
| 225 | * @return A new {@link SimpleObjectProperty} instance with a {@link File} | |
| 226 | * that references the given {@code path}. | |
| 227 | */ | |
| 228 | private SimpleObjectProperty<File> simpleFile( final String path ) { | |
| 229 | return new SimpleObjectProperty<>( new File( path ) ); | |
| 230 | } | |
| 231 | ||
| 232 | /** | |
| 233 | * Creates a label for the given key after interpolating its value. | |
| 234 | * | |
| 235 | * @param key The key to find in the resource bundle. | |
| 236 | * @return The value of the key as a label. | |
| 237 | */ | |
| 238 | private Node label( final String key ) { | |
| 239 | return new Label( get( key, true ) ); | |
| 240 | } | |
| 241 | ||
| 242 | /** | |
| 243 | * Creates a label for the given key. | |
| 244 | * | |
| 245 | * @param key The key to find in the resource bundle. | |
| 246 | * @param interpolate {@code true} means to interpolate the value. | |
| 247 | * @return The value of the key, interpolated if {@code interpolate} is | |
| 248 | * {@code true}. | |
| 249 | */ | |
| 250 | @SuppressWarnings("SameParameterValue") | |
| 251 | private Node label( final String key, final boolean interpolate ) { | |
| 252 | return new Label( get( key, interpolate ) ); | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Delegates to the {@link PreferencesFx} event handler for monitoring | |
| 257 | * save events. | |
| 258 | * | |
| 259 | * @param eventHandler The handler to call when the preferences are saved. | |
| 260 | */ | |
| 261 | public void addSaveEventHandler( | |
| 262 | final EventHandler<? super PreferencesFxEvent> eventHandler ) { | |
| 263 | final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED; | |
| 264 | getPreferencesFx().addEventHandler( eventType, eventHandler ); | |
| 265 | } | |
| 266 | ||
| 267 | /** | |
| 268 | * Returns the value for a key from the settings properties file. | |
| 269 | * | |
| 270 | * @param key Key within the settings properties file to find. | |
| 271 | * @param value Default value to return if the key is not found. | |
| 272 | * @return The value for the given key from the settings file, or the | |
| 273 | * given {@code value} if no key found. | |
| 274 | */ | |
| 275 | @SuppressWarnings("SameParameterValue") | |
| 276 | private String getSetting( final String key, final String value ) { | |
| 277 | return SETTINGS.getSetting( key, value ); | |
| 278 | } | |
| 279 | ||
| 280 | public ObjectProperty<File> definitionPathProperty() { | |
| 281 | return mPropDefinitionPath; | |
| 282 | } | |
| 283 | ||
| 284 | public Path getDefinitionPath() { | |
| 285 | return definitionPathProperty().getValue().toPath(); | |
| 286 | } | |
| 287 | ||
| 288 | private StringProperty defDelimiterBegan() { | |
| 289 | return mDefDelimiterBegan; | |
| 290 | } | |
| 291 | ||
| 292 | public String getDefDelimiterBegan() { | |
| 293 | return defDelimiterBegan().get(); | |
| 294 | } | |
| 295 | ||
| 296 | private StringProperty defDelimiterEnded() { | |
| 297 | return mDefDelimiterEnded; | |
| 298 | } | |
| 299 | ||
| 300 | public String getDefDelimiterEnded() { | |
| 301 | return defDelimiterEnded().get(); | |
| 302 | } | |
| 303 | ||
| 304 | public ObjectProperty<File> rDirectoryProperty() { | |
| 305 | return mPropRDirectory; | |
| 306 | } | |
| 307 | ||
| 308 | public File getRDirectory() { | |
| 309 | return rDirectoryProperty().getValue(); | |
| 310 | } | |
| 311 | ||
| 312 | public StringProperty rScriptProperty() { | |
| 313 | return mPropRScript; | |
| 314 | } | |
| 315 | ||
| 316 | public String getRScript() { | |
| 317 | return rScriptProperty().getValue(); | |
| 318 | } | |
| 319 | ||
| 320 | private StringProperty rDelimiterBegan() { | |
| 321 | return mRDelimiterBegan; | |
| 322 | } | |
| 323 | ||
| 324 | public String getRDelimiterBegan() { | |
| 325 | return rDelimiterBegan().get(); | |
| 326 | } | |
| 327 | ||
| 328 | private StringProperty rDelimiterEnded() { | |
| 329 | return mRDelimiterEnded; | |
| 330 | } | |
| 331 | ||
| 332 | public String getRDelimiterEnded() { | |
| 333 | return rDelimiterEnded().get(); | |
| 334 | } | |
| 335 | ||
| 336 | private ObjectProperty<File> imagesDirectoryProperty() { | |
| 337 | return mPropImagesDirectory; | |
| 338 | } | |
| 339 | ||
| 340 | public File getImagesDirectory() { | |
| 341 | return imagesDirectoryProperty().getValue(); | |
| 342 | } | |
| 343 | ||
| 344 | private StringProperty imagesOrderProperty() { | |
| 345 | return mPropImagesOrder; | |
| 346 | } | |
| 347 | ||
| 348 | public String getImagesOrder() { | |
| 349 | return imagesOrderProperty().getValue(); | |
| 350 | } | |
| 351 | ||
| 352 | public IntegerProperty fontsSizeEditorProperty() { | |
| 353 | return mPropFontsSizeEditor; | |
| 354 | } | |
| 355 | ||
| 356 | /** | |
| 357 | * Returns the preferred font size of the text editor. | |
| 358 | * | |
| 359 | * @return A non-negative integer, in points. | |
| 360 | */ | |
| 361 | public int getFontsSizeEditor() { | |
| 362 | return mPropFontsSizeEditor.intValue(); | |
| 363 | } | |
| 364 | ||
| 365 | private PreferencesFx getPreferencesFx() { | |
| 366 | return mPreferencesFx; | |
| 367 | } | |
| 368 | } | |
| 1 | 369 |
| 1 | /* | |
| 2 | * Copyright 2006 Patrick Wright | |
| 3 | * Copyright 2007 Wisconsin Court System | |
| 4 | * Copyright 2020 White Magic Software, Ltd. | |
| 5 | * | |
| 6 | * This program is free software; you can redistribute it and/or | |
| 7 | * modify it under the terms of the GNU Lesser General Public License | |
| 8 | * as published by the Free Software Foundation; either version 2.1 | |
| 9 | * of the License, or (at your option) any later version. | |
| 10 | * | |
| 11 | * This program is distributed in the hope that it will be useful, | |
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 14 | * GNU Lesser General Public License for more details. | |
| 15 | * | |
| 16 | * You should have received a copy of the GNU Lesser General Public License | |
| 17 | * along with this program; if not, write to the Free Software | |
| 18 | * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
| 19 | */ | |
| 20 | package com.keenwrite.preview; | |
| 21 | ||
| 22 | import com.keenwrite.adapters.ReplacedElementAdapter; | |
| 23 | import org.w3c.dom.Element; | |
| 24 | import org.xhtmlrenderer.extend.ReplacedElement; | |
| 25 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 26 | import org.xhtmlrenderer.extend.UserAgentCallback; | |
| 27 | import org.xhtmlrenderer.layout.LayoutContext; | |
| 28 | import org.xhtmlrenderer.render.BlockBox; | |
| 29 | ||
| 30 | import java.util.HashSet; | |
| 31 | import java.util.Set; | |
| 32 | ||
| 33 | public class ChainedReplacedElementFactory extends ReplacedElementAdapter { | |
| 34 | private final Set<ReplacedElementFactory> mFactoryList = new HashSet<>(); | |
| 35 | ||
| 36 | @Override | |
| 37 | public ReplacedElement createReplacedElement( | |
| 38 | final LayoutContext c, | |
| 39 | final BlockBox box, | |
| 40 | final UserAgentCallback uac, | |
| 41 | final int cssWidth, | |
| 42 | final int cssHeight ) { | |
| 43 | for( final var f : mFactoryList ) { | |
| 44 | final var r = f.createReplacedElement( | |
| 45 | c, box, uac, cssWidth, cssHeight ); | |
| 46 | ||
| 47 | if( r != null ) { | |
| 48 | return r; | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | return null; | |
| 53 | } | |
| 54 | ||
| 55 | @Override | |
| 56 | public void reset() { | |
| 57 | for( final var factory : mFactoryList ) { | |
| 58 | factory.reset(); | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | @Override | |
| 63 | public void remove( final Element element ) { | |
| 64 | for( final var factory : mFactoryList ) { | |
| 65 | factory.remove( element ); | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | public void addFactory( final ReplacedElementFactory factory ) { | |
| 70 | mFactoryList.add( factory ); | |
| 71 | } | |
| 72 | } | |
| 1 | 73 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preview; | |
| 29 | ||
| 30 | import com.keenwrite.exceptions.MissingFileException; | |
| 31 | import javafx.beans.property.IntegerProperty; | |
| 32 | import javafx.beans.property.SimpleIntegerProperty; | |
| 33 | import org.xhtmlrenderer.extend.FSImage; | |
| 34 | import org.xhtmlrenderer.resource.ImageResource; | |
| 35 | import org.xhtmlrenderer.swing.ImageResourceLoader; | |
| 36 | ||
| 37 | import javax.imageio.ImageIO; | |
| 38 | import java.net.URI; | |
| 39 | import java.net.URL; | |
| 40 | import java.nio.file.Paths; | |
| 41 | ||
| 42 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 43 | import static com.keenwrite.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER; | |
| 44 | import static com.keenwrite.util.ProtocolResolver.getProtocol; | |
| 45 | import static java.lang.String.valueOf; | |
| 46 | import static java.nio.file.Files.exists; | |
| 47 | import static org.xhtmlrenderer.swing.AWTFSImage.createImage; | |
| 48 | ||
| 49 | /** | |
| 50 | * Responsible for loading images. If the image cannot be found, a placeholder | |
| 51 | * is used instead. | |
| 52 | */ | |
| 53 | public class CustomImageLoader extends ImageResourceLoader { | |
| 54 | /** | |
| 55 | * Placeholder that's displayed when image cannot be found. | |
| 56 | */ | |
| 57 | private FSImage mBrokenImage; | |
| 58 | ||
| 59 | private final IntegerProperty mWidthProperty = new SimpleIntegerProperty(); | |
| 60 | ||
| 61 | /** | |
| 62 | * Gets an {@link IntegerProperty} that represents the maximum width an | |
| 63 | * image should be scaled. | |
| 64 | * | |
| 65 | * @return The maximum width for an image. | |
| 66 | */ | |
| 67 | public IntegerProperty widthProperty() { | |
| 68 | return mWidthProperty; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Gets an image resolved from the given URI. If the image cannot be found, | |
| 73 | * this will return a custom placeholder image indicating the reference | |
| 74 | * is broken. | |
| 75 | * | |
| 76 | * @param uri Path to the image resource to load. | |
| 77 | * @param width Ignored. | |
| 78 | * @param height Ignored. | |
| 79 | * @return The scaled image, or a placeholder image if the URI's content | |
| 80 | * could not be retrieved. | |
| 81 | */ | |
| 82 | @Override | |
| 83 | public synchronized ImageResource get( | |
| 84 | final String uri, final int width, final int height ) { | |
| 85 | assert uri != null; | |
| 86 | assert width >= 0; | |
| 87 | assert height >= 0; | |
| 88 | ||
| 89 | try { | |
| 90 | final var protocol = getProtocol( uri ); | |
| 91 | final ImageResource imageResource; | |
| 92 | ||
| 93 | if( protocol.isFile() ) { | |
| 94 | if( exists( Paths.get( new URI( uri ) ) ) ) { | |
| 95 | imageResource = super.get( uri, width, height ); | |
| 96 | } | |
| 97 | else { | |
| 98 | throw new MissingFileException( uri ); | |
| 99 | } | |
| 100 | } | |
| 101 | else if( protocol.isHttp() ) { | |
| 102 | // FlyingSaucer will silently swallow any images that fail to load. | |
| 103 | // Consequently, the following lines load the resource over HTTP and | |
| 104 | // translate errors into a broken image icon. | |
| 105 | final var url = new URL( uri ); | |
| 106 | final var image = ImageIO.read( url ); | |
| 107 | imageResource = new ImageResource( uri, createImage( image ) ); | |
| 108 | } | |
| 109 | else { | |
| 110 | // Caught below to return a broken image; exception is swallowed. | |
| 111 | throw new UnsupportedOperationException( valueOf( protocol ) ); | |
| 112 | } | |
| 113 | ||
| 114 | return scale( imageResource ); | |
| 115 | } catch( final Exception e ) { | |
| 116 | alert( e ); | |
| 117 | return new ImageResource( uri, getBrokenImage() ); | |
| 118 | } | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Scales the image found at the given URI. | |
| 123 | * | |
| 124 | * @param ir {@link ImageResource} of image loaded successfully. | |
| 125 | * @return Resource representing the rendered image and path. | |
| 126 | */ | |
| 127 | private ImageResource scale( final ImageResource ir ) { | |
| 128 | final var image = ir.getImage(); | |
| 129 | final var imageWidth = image.getWidth(); | |
| 130 | final var imageHeight = image.getHeight(); | |
| 131 | ||
| 132 | int maxWidth = mWidthProperty.get(); | |
| 133 | int newWidth = imageWidth; | |
| 134 | int newHeight = imageHeight; | |
| 135 | ||
| 136 | // Maintain aspect ratio while shrinking image to view port bounds. | |
| 137 | if( imageWidth > maxWidth ) { | |
| 138 | newWidth = maxWidth; | |
| 139 | newHeight = (newWidth * imageHeight) / imageWidth; | |
| 140 | } | |
| 141 | ||
| 142 | image.scale( newWidth, newHeight ); | |
| 143 | return ir; | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Lazily initializes the broken image placeholder. | |
| 148 | * | |
| 149 | * @return The {@link FSImage} that represents a broken image icon. | |
| 150 | */ | |
| 151 | private FSImage getBrokenImage() { | |
| 152 | final var image = mBrokenImage; | |
| 153 | ||
| 154 | if( image == null ) { | |
| 155 | mBrokenImage = createImage( BROKEN_IMAGE_PLACEHOLDER ); | |
| 156 | } | |
| 157 | ||
| 158 | return mBrokenImage; | |
| 159 | } | |
| 160 | } | |
| 1 | 161 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preview; | |
| 29 | ||
| 30 | import com.keenwrite.adapters.DocumentAdapter; | |
| 31 | import javafx.beans.property.BooleanProperty; | |
| 32 | import javafx.beans.property.SimpleBooleanProperty; | |
| 33 | import javafx.beans.value.ChangeListener; | |
| 34 | import javafx.beans.value.ObservableValue; | |
| 35 | import javafx.embed.swing.SwingNode; | |
| 36 | import javafx.scene.Node; | |
| 37 | import org.jsoup.Jsoup; | |
| 38 | import org.jsoup.helper.W3CDom; | |
| 39 | import org.jsoup.nodes.Document; | |
| 40 | import org.xhtmlrenderer.layout.SharedContext; | |
| 41 | import org.xhtmlrenderer.render.Box; | |
| 42 | import org.xhtmlrenderer.simple.XHTMLPanel; | |
| 43 | import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler; | |
| 44 | import org.xhtmlrenderer.swing.*; | |
| 45 | ||
| 46 | import javax.swing.*; | |
| 47 | import java.awt.*; | |
| 48 | import java.awt.event.ComponentAdapter; | |
| 49 | import java.awt.event.ComponentEvent; | |
| 50 | import java.net.URI; | |
| 51 | import java.nio.file.Path; | |
| 52 | ||
| 53 | import static com.keenwrite.Constants.*; | |
| 54 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 55 | import static com.keenwrite.util.ProtocolResolver.getProtocol; | |
| 56 | import static java.awt.Desktop.Action.BROWSE; | |
| 57 | import static java.awt.Desktop.getDesktop; | |
| 58 | import static java.lang.Math.max; | |
| 59 | import static javax.swing.SwingUtilities.invokeLater; | |
| 60 | import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER; | |
| 61 | ||
| 62 | /** | |
| 63 | * HTML preview pane is responsible for rendering an HTML document. | |
| 64 | */ | |
| 65 | public final class HTMLPreviewPane extends SwingNode { | |
| 66 | ||
| 67 | /** | |
| 68 | * Suppresses scrolling to the top on every key press. | |
| 69 | */ | |
| 70 | private static class HTMLPanel extends XHTMLPanel { | |
| 71 | @Override | |
| 72 | public void resetScrollPosition() { | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | /** | |
| 77 | * Suppresses scroll attempts until after the document has loaded. | |
| 78 | */ | |
| 79 | private static final class DocumentEventHandler extends DocumentAdapter { | |
| 80 | private final BooleanProperty mReadyProperty = new SimpleBooleanProperty(); | |
| 81 | ||
| 82 | public BooleanProperty readyProperty() { | |
| 83 | return mReadyProperty; | |
| 84 | } | |
| 85 | ||
| 86 | @Override | |
| 87 | public void documentStarted() { | |
| 88 | mReadyProperty.setValue( Boolean.FALSE ); | |
| 89 | } | |
| 90 | ||
| 91 | @Override | |
| 92 | public void documentLoaded() { | |
| 93 | mReadyProperty.setValue( Boolean.TRUE ); | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | /** | |
| 98 | * Ensure that images are constrained to the panel width upon resizing. | |
| 99 | */ | |
| 100 | private final class ResizeListener extends ComponentAdapter { | |
| 101 | @Override | |
| 102 | public void componentResized( final ComponentEvent e ) { | |
| 103 | setWidth( e ); | |
| 104 | } | |
| 105 | ||
| 106 | @Override | |
| 107 | public void componentShown( final ComponentEvent e ) { | |
| 108 | setWidth( e ); | |
| 109 | } | |
| 110 | ||
| 111 | /** | |
| 112 | * Sets the width of the {@link HTMLPreviewPane} so that images can be | |
| 113 | * scaled to fit. The scale factor is adjusted a bit below the full width | |
| 114 | * to prevent the horizontal scrollbar from appearing. | |
| 115 | * | |
| 116 | * @param event The component that defines the image scaling width. | |
| 117 | */ | |
| 118 | private void setWidth( final ComponentEvent event ) { | |
| 119 | final int width = (int) (event.getComponent().getWidth() * .95); | |
| 120 | HTMLPreviewPane.this.mImageLoader.widthProperty().set( width ); | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | /** | |
| 125 | * Responsible for opening hyperlinks. External hyperlinks are opened in | |
| 126 | * the system's default browser; local file system links are opened in the | |
| 127 | * editor. | |
| 128 | */ | |
| 129 | private static class HyperlinkListener extends LinkListener { | |
| 130 | @Override | |
| 131 | public void linkClicked( final BasicPanel panel, final String link ) { | |
| 132 | try { | |
| 133 | final var protocol = getProtocol( link ); | |
| 134 | ||
| 135 | switch( protocol ) { | |
| 136 | case HTTP: | |
| 137 | final var desktop = getDesktop(); | |
| 138 | ||
| 139 | if( desktop.isSupported( BROWSE ) ) { | |
| 140 | desktop.browse( new URI( link ) ); | |
| 141 | } | |
| 142 | break; | |
| 143 | case FILE: | |
| 144 | // TODO: #88 -- publish a message to the event bus. | |
| 145 | break; | |
| 146 | } | |
| 147 | } catch( final Exception ex ) { | |
| 148 | alert( ex ); | |
| 149 | } | |
| 150 | } | |
| 151 | } | |
| 152 | ||
| 153 | /** | |
| 154 | * The CSS must be rendered in points (pt) not pixels (px) to avoid blurry | |
| 155 | * rendering on some platforms. | |
| 156 | */ | |
| 157 | private static final String HTML_PREFIX = "<!DOCTYPE html>" | |
| 158 | + "<html>" | |
| 159 | + "<head>" | |
| 160 | + "<link rel='stylesheet' href='" + | |
| 161 | HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) + "'/>" | |
| 162 | + "</head>" | |
| 163 | + "<body>"; | |
| 164 | ||
| 165 | private static final W3CDom W3C_DOM = new W3CDom(); | |
| 166 | private static final XhtmlNamespaceHandler NS_HANDLER = | |
| 167 | new XhtmlNamespaceHandler(); | |
| 168 | ||
| 169 | private final StringBuilder mHtmlDocument = new StringBuilder( 65536 ); | |
| 170 | private final int mHtmlPrefixLength; | |
| 171 | ||
| 172 | private final HTMLPanel mHtmlRenderer = new HTMLPanel(); | |
| 173 | private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer ); | |
| 174 | private final DocumentEventHandler mDocHandler = new DocumentEventHandler(); | |
| 175 | private final CustomImageLoader mImageLoader = new CustomImageLoader(); | |
| 176 | ||
| 177 | private Path mPath = DEFAULT_DIRECTORY; | |
| 178 | ||
| 179 | /** | |
| 180 | * Creates a new preview pane that can scroll to the caret position within the | |
| 181 | * document. | |
| 182 | */ | |
| 183 | public HTMLPreviewPane() { | |
| 184 | setStyle( "-fx-background-color: white;" ); | |
| 185 | ||
| 186 | // No need to append same prefix each time the HTML content is updated. | |
| 187 | mHtmlDocument.append( HTML_PREFIX ); | |
| 188 | mHtmlPrefixLength = mHtmlDocument.length(); | |
| 189 | ||
| 190 | // Inject an SVG renderer that produces high-quality SVG buffered images. | |
| 191 | final var factory = new ChainedReplacedElementFactory(); | |
| 192 | factory.addFactory( new SvgReplacedElementFactory() ); | |
| 193 | factory.addFactory( new SwingReplacedElementFactory( | |
| 194 | NO_OP_REPAINT_LISTENER, mImageLoader ) ); | |
| 195 | ||
| 196 | final var context = getSharedContext(); | |
| 197 | final var textRenderer = context.getTextRenderer(); | |
| 198 | context.setReplacedElementFactory( factory ); | |
| 199 | textRenderer.setSmoothingThreshold( 0 ); | |
| 200 | ||
| 201 | setContent( mScrollPane ); | |
| 202 | mHtmlRenderer.addDocumentListener( mDocHandler ); | |
| 203 | mHtmlRenderer.addComponentListener( new ResizeListener() ); | |
| 204 | ||
| 205 | // The default mouse click listener attempts navigation within the | |
| 206 | // preview panel. We want to usurp that behaviour to open the link in | |
| 207 | // a platform-specific browser. | |
| 208 | for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) { | |
| 209 | if( !(listener instanceof HoverListener) ) { | |
| 210 | mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener ); | |
| 211 | } | |
| 212 | } | |
| 213 | ||
| 214 | mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() ); | |
| 215 | } | |
| 216 | ||
| 217 | /** | |
| 218 | * Updates the internal HTML source, loads it into the preview pane, then | |
| 219 | * scrolls to the caret position. | |
| 220 | * | |
| 221 | * @param html The new HTML document to display. | |
| 222 | */ | |
| 223 | public void process( final String html ) { | |
| 224 | final Document jsoupDoc = Jsoup.parse( decorate( html ) ); | |
| 225 | final org.w3c.dom.Document w3cDoc = W3C_DOM.fromJsoup( jsoupDoc ); | |
| 226 | ||
| 227 | ||
| 228 | // Access to a Swing component must occur from the Event Dispatch | |
| 229 | // thread according to Swing threading restrictions. | |
| 230 | invokeLater( | |
| 231 | () -> mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), NS_HANDLER ) | |
| 232 | ); | |
| 233 | } | |
| 234 | ||
| 235 | public void clear() { | |
| 236 | process( "" ); | |
| 237 | } | |
| 238 | ||
| 239 | /** | |
| 240 | * Scrolls to an anchor link. The anchor links are injected when the | |
| 241 | * HTML document is created. | |
| 242 | * | |
| 243 | * @param id The unique anchor link identifier. | |
| 244 | */ | |
| 245 | public void tryScrollTo( final int id ) { | |
| 246 | final ChangeListener<Boolean> listener = new ChangeListener<>() { | |
| 247 | @Override | |
| 248 | public void changed( | |
| 249 | final ObservableValue<? extends Boolean> observable, | |
| 250 | final Boolean oldValue, | |
| 251 | final Boolean newValue ) { | |
| 252 | if( newValue ) { | |
| 253 | scrollTo( id ); | |
| 254 | ||
| 255 | mDocHandler.readyProperty().removeListener( this ); | |
| 256 | } | |
| 257 | } | |
| 258 | }; | |
| 259 | ||
| 260 | mDocHandler.readyProperty().addListener( listener ); | |
| 261 | } | |
| 262 | ||
| 263 | /** | |
| 264 | * Scrolls to the closest element matching the given identifier without | |
| 265 | * waiting for the document to be ready. Be sure the document is ready | |
| 266 | * before calling this method. | |
| 267 | * | |
| 268 | * @param id Paragraph index. | |
| 269 | */ | |
| 270 | public void scrollTo( final int id ) { | |
| 271 | if( id < 2 ) { | |
| 272 | scrollToTop(); | |
| 273 | } | |
| 274 | else { | |
| 275 | Box box = findPrevBox( id ); | |
| 276 | box = box == null ? findNextBox( id + 1 ) : box; | |
| 277 | ||
| 278 | if( box == null ) { | |
| 279 | scrollToBottom(); | |
| 280 | } | |
| 281 | else { | |
| 282 | scrollTo( box ); | |
| 283 | } | |
| 284 | } | |
| 285 | } | |
| 286 | ||
| 287 | private Box findPrevBox( final int id ) { | |
| 288 | int prevId = id; | |
| 289 | Box box = null; | |
| 290 | ||
| 291 | while( prevId > 0 && (box = getBoxById( PARAGRAPH_ID_PREFIX + prevId )) == null ) { | |
| 292 | prevId--; | |
| 293 | } | |
| 294 | ||
| 295 | return box; | |
| 296 | } | |
| 297 | ||
| 298 | private Box findNextBox( final int id ) { | |
| 299 | int nextId = id; | |
| 300 | Box box = null; | |
| 301 | ||
| 302 | while( nextId - id < 5 && | |
| 303 | (box = getBoxById( PARAGRAPH_ID_PREFIX + nextId )) == null ) { | |
| 304 | nextId++; | |
| 305 | } | |
| 306 | ||
| 307 | return box; | |
| 308 | } | |
| 309 | ||
| 310 | private void scrollTo( final Point point ) { | |
| 311 | invokeLater( () -> mHtmlRenderer.scrollTo( point ) ); | |
| 312 | } | |
| 313 | ||
| 314 | private void scrollTo( final Box box ) { | |
| 315 | scrollTo( createPoint( box ) ); | |
| 316 | } | |
| 317 | ||
| 318 | private void scrollToY( final int y ) { | |
| 319 | scrollTo( new Point( 0, y ) ); | |
| 320 | } | |
| 321 | ||
| 322 | private void scrollToTop() { | |
| 323 | scrollToY( 0 ); | |
| 324 | } | |
| 325 | ||
| 326 | private void scrollToBottom() { | |
| 327 | scrollToY( mHtmlRenderer.getHeight() ); | |
| 328 | } | |
| 329 | ||
| 330 | private Box getBoxById( final String id ) { | |
| 331 | return getSharedContext().getBoxById( id ); | |
| 332 | } | |
| 333 | ||
| 334 | private String decorate( final String html ) { | |
| 335 | // Trim the HTML back to only the prefix. | |
| 336 | mHtmlDocument.setLength( mHtmlPrefixLength ); | |
| 337 | ||
| 338 | // Write the HTML body element followed by closing tags. | |
| 339 | return mHtmlDocument.append( html ).toString(); | |
| 340 | } | |
| 341 | ||
| 342 | public Path getPath() { | |
| 343 | return mPath; | |
| 344 | } | |
| 345 | ||
| 346 | public void setPath( final Path path ) { | |
| 347 | assert path != null; | |
| 348 | mPath = path; | |
| 349 | } | |
| 350 | ||
| 351 | /** | |
| 352 | * Content to embed in a panel. | |
| 353 | * | |
| 354 | * @return The content to display to the user. | |
| 355 | */ | |
| 356 | public Node getNode() { | |
| 357 | return this; | |
| 358 | } | |
| 359 | ||
| 360 | public JScrollPane getScrollPane() { | |
| 361 | return mScrollPane; | |
| 362 | } | |
| 363 | ||
| 364 | public JScrollBar getVerticalScrollBar() { | |
| 365 | return getScrollPane().getVerticalScrollBar(); | |
| 366 | } | |
| 367 | ||
| 368 | /** | |
| 369 | * Creates a {@link Point} to use as a reference for scrolling to the area | |
| 370 | * described by the given {@link Box}. The {@link Box} coordinates are used | |
| 371 | * to populate the {@link Point}'s location, with minor adjustments for | |
| 372 | * vertical centering. | |
| 373 | * | |
| 374 | * @param box The {@link Box} that represents a scrolling anchor reference. | |
| 375 | * @return A coordinate suitable for scrolling to. | |
| 376 | */ | |
| 377 | private Point createPoint( final Box box ) { | |
| 378 | assert box != null; | |
| 379 | ||
| 380 | int x = box.getAbsX(); | |
| 381 | ||
| 382 | // Scroll back up by half the height of the scroll bar to keep the typing | |
| 383 | // area within the view port. Otherwise the view port will have jumped too | |
| 384 | // high up and the whatever gets typed won't be visible. | |
| 385 | int y = max( | |
| 386 | box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2), | |
| 387 | 0 ); | |
| 388 | ||
| 389 | if( !box.getStyle().isInline() ) { | |
| 390 | final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() ); | |
| 391 | x += margin.left(); | |
| 392 | y += margin.top(); | |
| 393 | } | |
| 394 | ||
| 395 | return new Point( x, y ); | |
| 396 | } | |
| 397 | ||
| 398 | private String getBaseUrl() { | |
| 399 | final Path basePath = getPath(); | |
| 400 | final Path parent = basePath == null ? null : basePath.getParent(); | |
| 401 | ||
| 402 | return parent == null ? "" : parent.toUri().toString(); | |
| 403 | } | |
| 404 | ||
| 405 | private SharedContext getSharedContext() { | |
| 406 | return mHtmlRenderer.getSharedContext(); | |
| 407 | } | |
| 408 | } | |
| 1 | 409 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preview; | |
| 29 | ||
| 30 | import com.whitemagicsoftware.tex.*; | |
| 31 | import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D; | |
| 32 | import org.w3c.dom.Document; | |
| 33 | ||
| 34 | import java.util.function.Supplier; | |
| 35 | ||
| 36 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 37 | ||
| 38 | /** | |
| 39 | * Responsible for rendering formulas as scalable vector graphics (SVG). | |
| 40 | */ | |
| 41 | public class MathRenderer { | |
| 42 | ||
| 43 | /** | |
| 44 | * Default font size in points. | |
| 45 | */ | |
| 46 | private static final float FONT_SIZE = 20f; | |
| 47 | ||
| 48 | private final TeXFont mTeXFont = createDefaultTeXFont( FONT_SIZE ); | |
| 49 | private final TeXEnvironment mEnvironment = createTeXEnvironment( mTeXFont ); | |
| 50 | private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D(); | |
| 51 | ||
| 52 | public MathRenderer() { | |
| 53 | mGraphics.scale( FONT_SIZE, FONT_SIZE ); | |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * This method only takes a few seconds to generate | |
| 58 | * | |
| 59 | * @param equation A mathematical expression to render. | |
| 60 | * @return The given string with all formulas transformed into SVG format. | |
| 61 | */ | |
| 62 | public Document render( final String equation ) { | |
| 63 | final var formula = new TeXFormula( equation ); | |
| 64 | final var box = formula.createBox( mEnvironment ); | |
| 65 | final var l = new TeXLayout( box, FONT_SIZE ); | |
| 66 | ||
| 67 | mGraphics.initialize( l.getWidth(), l.getHeight() ); | |
| 68 | box.draw( mGraphics, l.getX(), l.getY() ); | |
| 69 | return mGraphics.toDom(); | |
| 70 | } | |
| 71 | ||
| 72 | @SuppressWarnings("SameParameterValue") | |
| 73 | private TeXFont createDefaultTeXFont( final float fontSize ) { | |
| 74 | return create( () -> new DefaultTeXFont( fontSize ) ); | |
| 75 | } | |
| 76 | ||
| 77 | private TeXEnvironment createTeXEnvironment( final TeXFont texFont ) { | |
| 78 | return create( () -> new TeXEnvironment( texFont ) ); | |
| 79 | } | |
| 80 | ||
| 81 | private SvgDomGraphics2D createSvgDomGraphics2D() { | |
| 82 | return create( SvgDomGraphics2D::new ); | |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * Tries to instantiate a given object, returning {@code null} on failure. | |
| 87 | * The failure message is bubbled up to to the user interface. | |
| 88 | * | |
| 89 | * @param supplier Creates an instance. | |
| 90 | * @param <T> The type of instance being created. | |
| 91 | * @return An instance of the parameterized type or {@code null} upon error. | |
| 92 | */ | |
| 93 | private <T> T create( final Supplier<T> supplier ) { | |
| 94 | try { | |
| 95 | return supplier.get(); | |
| 96 | } catch( final Exception ex ) { | |
| 97 | alert( ex ); | |
| 98 | return null; | |
| 99 | } | |
| 100 | } | |
| 101 | } | |
| 1 | 102 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preview; | |
| 29 | ||
| 30 | import java.util.HashMap; | |
| 31 | import java.util.Map; | |
| 32 | ||
| 33 | import static java.awt.RenderingHints.*; | |
| 34 | import static java.awt.Toolkit.getDefaultToolkit; | |
| 35 | ||
| 36 | /** | |
| 37 | * Responsible for supplying consistent rendering hints throughout the | |
| 38 | * application, such as image rendering for {@link SvgRasterizer}. | |
| 39 | */ | |
| 40 | @SuppressWarnings("rawtypes") | |
| 41 | public class RenderingSettings { | |
| 42 | ||
| 43 | /** | |
| 44 | * Default hints for high-quality rendering that may be changed by | |
| 45 | * the system's rendering hints. | |
| 46 | */ | |
| 47 | private static final Map<Object, Object> DEFAULT_HINTS = Map.of( | |
| 48 | KEY_ANTIALIASING, | |
| 49 | VALUE_ANTIALIAS_ON, | |
| 50 | KEY_ALPHA_INTERPOLATION, | |
| 51 | VALUE_ALPHA_INTERPOLATION_QUALITY, | |
| 52 | KEY_COLOR_RENDERING, | |
| 53 | VALUE_COLOR_RENDER_QUALITY, | |
| 54 | KEY_DITHERING, | |
| 55 | VALUE_DITHER_DISABLE, | |
| 56 | KEY_FRACTIONALMETRICS, | |
| 57 | VALUE_FRACTIONALMETRICS_ON, | |
| 58 | KEY_INTERPOLATION, | |
| 59 | VALUE_INTERPOLATION_BICUBIC, | |
| 60 | KEY_RENDERING, | |
| 61 | VALUE_RENDER_QUALITY, | |
| 62 | KEY_STROKE_CONTROL, | |
| 63 | VALUE_STROKE_PURE, | |
| 64 | KEY_TEXT_ANTIALIASING, | |
| 65 | VALUE_TEXT_ANTIALIAS_ON | |
| 66 | ); | |
| 67 | ||
| 68 | /** | |
| 69 | * Shared hints for high-quality rendering. | |
| 70 | */ | |
| 71 | public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>( | |
| 72 | DEFAULT_HINTS | |
| 73 | ); | |
| 74 | ||
| 75 | static { | |
| 76 | final var toolkit = getDefaultToolkit(); | |
| 77 | final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" ); | |
| 78 | ||
| 79 | if( hints instanceof Map ) { | |
| 80 | final var map = (Map) hints; | |
| 81 | for( final var key : map.keySet() ) { | |
| 82 | final var hint = map.get( key ); | |
| 83 | RENDERING_HINTS.put( key, hint ); | |
| 84 | } | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Prevent instantiation as per Joshua Bloch's recommendation. | |
| 90 | */ | |
| 91 | private RenderingSettings() { | |
| 92 | } | |
| 93 | } | |
| 1 | 94 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preview; | |
| 29 | ||
| 30 | import org.apache.batik.anim.dom.SAXSVGDocumentFactory; | |
| 31 | import org.apache.batik.gvt.renderer.ImageRenderer; | |
| 32 | import org.apache.batik.transcoder.TranscoderException; | |
| 33 | import org.apache.batik.transcoder.TranscoderInput; | |
| 34 | import org.apache.batik.transcoder.TranscoderOutput; | |
| 35 | import org.apache.batik.transcoder.image.ImageTranscoder; | |
| 36 | import org.w3c.dom.Document; | |
| 37 | import org.w3c.dom.Element; | |
| 38 | ||
| 39 | import javax.xml.transform.Transformer; | |
| 40 | import javax.xml.transform.TransformerConfigurationException; | |
| 41 | import javax.xml.transform.TransformerFactory; | |
| 42 | import javax.xml.transform.dom.DOMSource; | |
| 43 | import javax.xml.transform.stream.StreamResult; | |
| 44 | import java.awt.*; | |
| 45 | import java.awt.image.BufferedImage; | |
| 46 | import java.io.IOException; | |
| 47 | import java.io.StringReader; | |
| 48 | import java.io.StringWriter; | |
| 49 | import java.net.URL; | |
| 50 | import java.text.NumberFormat; | |
| 51 | ||
| 52 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 53 | import static com.keenwrite.preview.RenderingSettings.RENDERING_HINTS; | |
| 54 | import static java.awt.image.BufferedImage.TYPE_INT_RGB; | |
| 55 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 56 | import static java.text.NumberFormat.getIntegerInstance; | |
| 57 | import static javax.xml.transform.OutputKeys.*; | |
| 58 | import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH; | |
| 59 | import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName; | |
| 60 | ||
| 61 | /** | |
| 62 | * Responsible for converting SVG images into rasterized PNG images. | |
| 63 | */ | |
| 64 | public class SvgRasterizer { | |
| 65 | private static final SAXSVGDocumentFactory FACTORY_DOM = | |
| 66 | new SAXSVGDocumentFactory( getXMLParserClassName() ); | |
| 67 | ||
| 68 | private static final TransformerFactory FACTORY_TRANSFORM = | |
| 69 | TransformerFactory.newInstance(); | |
| 70 | ||
| 71 | private static final Transformer sTransformer; | |
| 72 | ||
| 73 | static { | |
| 74 | Transformer t; | |
| 75 | ||
| 76 | try { | |
| 77 | t = FACTORY_TRANSFORM.newTransformer(); | |
| 78 | t.setOutputProperty( OMIT_XML_DECLARATION, "yes" ); | |
| 79 | t.setOutputProperty( METHOD, "xml" ); | |
| 80 | t.setOutputProperty( INDENT, "no" ); | |
| 81 | t.setOutputProperty( ENCODING, UTF_8.name() ); | |
| 82 | } catch( final TransformerConfigurationException e ) { | |
| 83 | t = null; | |
| 84 | } | |
| 85 | ||
| 86 | sTransformer = t; | |
| 87 | } | |
| 88 | ||
| 89 | private static final NumberFormat INT_FORMAT = getIntegerInstance(); | |
| 90 | ||
| 91 | public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER; | |
| 92 | ||
| 93 | /** | |
| 94 | * A FontAwesome camera icon, cleft asunder. | |
| 95 | */ | |
| 96 | public static final String BROKEN_IMAGE_SVG = | |
| 97 | "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" + | |
| 98 | ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" + | |
| 99 | ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " + | |
| 100 | "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" + | |
| 101 | ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" + | |
| 102 | ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" + | |
| 103 | ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" + | |
| 104 | ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " + | |
| 105 | "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" + | |
| 106 | ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" + | |
| 107 | ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" + | |
| 108 | ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" + | |
| 109 | ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" + | |
| 110 | ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" + | |
| 111 | ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" + | |
| 112 | ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" + | |
| 113 | ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " + | |
| 114 | ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" + | |
| 115 | ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" + | |
| 116 | ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" + | |
| 117 | ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" + | |
| 118 | ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" + | |
| 119 | ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" + | |
| 120 | ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " + | |
| 121 | "0'/></g></svg>"; | |
| 122 | ||
| 123 | static { | |
| 124 | // The width and height cannot be embedded in the SVG above because the | |
| 125 | // path element values are relative to the viewBox dimensions. | |
| 126 | final int w = 75; | |
| 127 | final int h = 75; | |
| 128 | BufferedImage image; | |
| 129 | ||
| 130 | try { | |
| 131 | image = rasterizeString( BROKEN_IMAGE_SVG, w ); | |
| 132 | } catch( final Exception e ) { | |
| 133 | image = new BufferedImage( w, h, TYPE_INT_RGB ); | |
| 134 | final var graphics = (Graphics2D) image.getGraphics(); | |
| 135 | graphics.setRenderingHints( RENDERING_HINTS ); | |
| 136 | ||
| 137 | // Fall back to a (\) symbol. | |
| 138 | graphics.setColor( new Color( 204, 204, 204 ) ); | |
| 139 | graphics.fillRect( 0, 0, w, h ); | |
| 140 | graphics.setColor( new Color( 255, 204, 204 ) ); | |
| 141 | graphics.setStroke( new BasicStroke( 4 ) ); | |
| 142 | graphics.drawOval( w / 4, h / 4, w / 2, h / 2 ); | |
| 143 | graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI), | |
| 144 | h / 4 + (int) (w / 4 / Math.PI), | |
| 145 | w / 2 + w / 4 - (int) (w / 4 / Math.PI), | |
| 146 | h / 2 + h / 4 - (int) (w / 4 / Math.PI) ); | |
| 147 | } | |
| 148 | ||
| 149 | BROKEN_IMAGE_PLACEHOLDER = image; | |
| 150 | } | |
| 151 | ||
| 152 | /** | |
| 153 | * Responsible for creating a new {@link ImageRenderer} implementation that | |
| 154 | * can render a DOM as an SVG image. | |
| 155 | */ | |
| 156 | private static class BufferedImageTranscoder extends ImageTranscoder { | |
| 157 | private BufferedImage mImage; | |
| 158 | ||
| 159 | @Override | |
| 160 | public BufferedImage createImage( final int w, final int h ) { | |
| 161 | return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB ); | |
| 162 | } | |
| 163 | ||
| 164 | @Override | |
| 165 | public void writeImage( | |
| 166 | final BufferedImage image, final TranscoderOutput output ) { | |
| 167 | mImage = image; | |
| 168 | } | |
| 169 | ||
| 170 | public BufferedImage getImage() { | |
| 171 | return mImage; | |
| 172 | } | |
| 173 | ||
| 174 | @Override | |
| 175 | protected ImageRenderer createRenderer() { | |
| 176 | final ImageRenderer renderer = super.createRenderer(); | |
| 177 | final RenderingHints hints = renderer.getRenderingHints(); | |
| 178 | hints.putAll( RENDERING_HINTS ); | |
| 179 | ||
| 180 | renderer.setRenderingHints( hints ); | |
| 181 | ||
| 182 | return renderer; | |
| 183 | } | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * Rasterizes the vector graphic file at the given URL. If any exception | |
| 188 | * happens, a red circle is returned instead. | |
| 189 | * | |
| 190 | * @param url The URL to a vector graphic file, which must include the | |
| 191 | * protocol scheme (such as file:// or https://). | |
| 192 | * @param width The number of pixels wide to render the image. The aspect | |
| 193 | * ratio is maintained. | |
| 194 | * @return Either the rasterized image upon success or a red circle. | |
| 195 | */ | |
| 196 | public static BufferedImage rasterize( final String url, final int width ) { | |
| 197 | try { | |
| 198 | return rasterize( new URL( url ), width ); | |
| 199 | } catch( final Exception ex ) { | |
| 200 | alert( ex ); | |
| 201 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 202 | } | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * Rasterizes the given document into an image. | |
| 207 | * | |
| 208 | * @param svg The SVG {@link Document} to rasterize. | |
| 209 | * @param width The rasterized image's width (in pixels). | |
| 210 | * @return The rasterized image. | |
| 211 | * @throws TranscoderException Signifies an issue with the input document. | |
| 212 | */ | |
| 213 | public static BufferedImage rasterize( final Document svg, final int width ) | |
| 214 | throws TranscoderException { | |
| 215 | final var transcoder = new BufferedImageTranscoder(); | |
| 216 | final var input = new TranscoderInput( svg ); | |
| 217 | ||
| 218 | transcoder.addTranscodingHint( KEY_WIDTH, (float) width ); | |
| 219 | transcoder.transcode( input, null ); | |
| 220 | ||
| 221 | return transcoder.getImage(); | |
| 222 | } | |
| 223 | ||
| 224 | /** | |
| 225 | * Converts an SVG drawing into a rasterized image that can be drawn on | |
| 226 | * a graphics context. | |
| 227 | * | |
| 228 | * @param url The path to the image (can be web address). | |
| 229 | * @param width Scale the image width to this size (aspect ratio is | |
| 230 | * maintained). | |
| 231 | * @return The vector graphic transcoded into a raster image format. | |
| 232 | * @throws IOException Could not read the vector graphic. | |
| 233 | * @throws TranscoderException Could not convert the vector graphic to an | |
| 234 | * instance of {@link Image}. | |
| 235 | */ | |
| 236 | public static BufferedImage rasterize( final URL url, final int width ) | |
| 237 | throws IOException, TranscoderException { | |
| 238 | return rasterize( FACTORY_DOM.createDocument( url.toString() ), width ); | |
| 239 | } | |
| 240 | ||
| 241 | public static BufferedImage rasterize( final Document document ) { | |
| 242 | try { | |
| 243 | final var root = document.getDocumentElement(); | |
| 244 | final var width = root.getAttribute( "width" ); | |
| 245 | return rasterize( document, INT_FORMAT.parse( width ).intValue() ); | |
| 246 | } catch( final Exception ex ) { | |
| 247 | alert( ex ); | |
| 248 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 249 | } | |
| 250 | } | |
| 251 | ||
| 252 | /** | |
| 253 | * Converts an SVG string into a rasterized image that can be drawn on | |
| 254 | * a graphics context. | |
| 255 | * | |
| 256 | * @param svg The SVG xml document. | |
| 257 | * @param w Scale the image width to this size (aspect ratio is | |
| 258 | * maintained). | |
| 259 | * @return The vector graphic transcoded into a raster image format. | |
| 260 | * @throws TranscoderException Could not convert the vector graphic to an | |
| 261 | * instance of {@link Image}. | |
| 262 | */ | |
| 263 | public static BufferedImage rasterizeString( final String svg, final int w ) | |
| 264 | throws IOException, TranscoderException { | |
| 265 | return rasterize( toDocument( svg ), w ); | |
| 266 | } | |
| 267 | ||
| 268 | /** | |
| 269 | * Converts an SVG string into a rasterized image that can be drawn on | |
| 270 | * a graphics context. The dimensions are determined from the document. | |
| 271 | * | |
| 272 | * @param xml The SVG xml document. | |
| 273 | * @return The vector graphic transcoded into a raster image format. | |
| 274 | */ | |
| 275 | public static BufferedImage rasterizeString( final String xml ) { | |
| 276 | try { | |
| 277 | final var document = toDocument( xml ); | |
| 278 | final var root = document.getDocumentElement(); | |
| 279 | final var width = root.getAttribute( "width" ); | |
| 280 | return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() ); | |
| 281 | } catch( final Exception ex ) { | |
| 282 | alert( ex ); | |
| 283 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 284 | } | |
| 285 | } | |
| 286 | ||
| 287 | /** | |
| 288 | * Converts an SVG XML string into a new {@link Document} instance. | |
| 289 | * | |
| 290 | * @param xml The XML containing SVG elements. | |
| 291 | * @return The SVG contents parsed into a {@link Document} object model. | |
| 292 | * @throws IOException Could | |
| 293 | */ | |
| 294 | private static Document toDocument( final String xml ) throws IOException { | |
| 295 | try( final var reader = new StringReader( xml ) ) { | |
| 296 | return FACTORY_DOM.createSVGDocument( | |
| 297 | "http://www.w3.org/2000/svg", reader ); | |
| 298 | } | |
| 299 | } | |
| 300 | ||
| 301 | /** | |
| 302 | * Given a document object model (DOM) {@link Element}, this will convert that | |
| 303 | * element to a string. | |
| 304 | * | |
| 305 | * @param e The DOM node to convert to a string. | |
| 306 | * @return The DOM node as an escaped, plain text string. | |
| 307 | */ | |
| 308 | public static String toSvg( final Element e ) { | |
| 309 | try( final var writer = new StringWriter() ) { | |
| 310 | sTransformer.transform( new DOMSource( e ), new StreamResult( writer ) ); | |
| 311 | return writer.toString().replaceAll( "xmlns=\"\" ", "" ); | |
| 312 | } catch( final Exception ex ) { | |
| 313 | alert( ex ); | |
| 314 | } | |
| 315 | ||
| 316 | return BROKEN_IMAGE_SVG; | |
| 317 | } | |
| 318 | } | |
| 1 | 319 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.preview; | |
| 29 | ||
| 30 | import com.keenwrite.util.BoundedCache; | |
| 31 | import org.apache.commons.io.FilenameUtils; | |
| 32 | import org.w3c.dom.Element; | |
| 33 | import org.xhtmlrenderer.extend.ReplacedElement; | |
| 34 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 35 | import org.xhtmlrenderer.extend.UserAgentCallback; | |
| 36 | import org.xhtmlrenderer.layout.LayoutContext; | |
| 37 | import org.xhtmlrenderer.render.BlockBox; | |
| 38 | import org.xhtmlrenderer.simple.extend.FormSubmissionListener; | |
| 39 | import org.xhtmlrenderer.swing.ImageReplacedElement; | |
| 40 | ||
| 41 | import java.awt.image.BufferedImage; | |
| 42 | import java.util.Map; | |
| 43 | import java.util.function.Function; | |
| 44 | ||
| 45 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 46 | import static com.keenwrite.preview.SvgRasterizer.rasterize; | |
| 47 | import static com.keenwrite.processors.markdown.tex.TeXNode.HTML_TEX; | |
| 48 | ||
| 49 | /** | |
| 50 | * Responsible for running {@link SvgRasterizer} on SVG images detected within | |
| 51 | * a document to transform them into rasterized versions. | |
| 52 | */ | |
| 53 | public class SvgReplacedElementFactory implements ReplacedElementFactory { | |
| 54 | ||
| 55 | /** | |
| 56 | * Prevent instantiation until needed. | |
| 57 | */ | |
| 58 | private static class MathRendererContainer { | |
| 59 | private static final MathRenderer INSTANCE = new MathRenderer(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Returns the singleton instance for rendering math symbols. | |
| 64 | * | |
| 65 | * @return A non-null instance, loaded, configured, and ready to render math. | |
| 66 | */ | |
| 67 | public static MathRenderer getInstance() { | |
| 68 | return MathRendererContainer.INSTANCE; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * SVG filename extension maps to an SVG image element. | |
| 73 | */ | |
| 74 | private static final String SVG_FILE = "svg"; | |
| 75 | ||
| 76 | private static final String HTML_IMAGE = "img"; | |
| 77 | private static final String HTML_IMAGE_SRC = "src"; | |
| 78 | ||
| 79 | /** | |
| 80 | * A bounded cache that removes the oldest image if the maximum number of | |
| 81 | * cached images has been reached. This constrains the number of images | |
| 82 | * loaded into memory. | |
| 83 | */ | |
| 84 | private final Map<String, BufferedImage> mImageCache = | |
| 85 | new BoundedCache<>( 150 ); | |
| 86 | ||
| 87 | @Override | |
| 88 | public ReplacedElement createReplacedElement( | |
| 89 | final LayoutContext c, | |
| 90 | final BlockBox box, | |
| 91 | final UserAgentCallback uac, | |
| 92 | final int cssWidth, | |
| 93 | final int cssHeight ) { | |
| 94 | BufferedImage image = null; | |
| 95 | final var e = box.getElement(); | |
| 96 | ||
| 97 | if( e != null ) { | |
| 98 | try { | |
| 99 | final var nodeName = e.getNodeName(); | |
| 100 | ||
| 101 | if( HTML_IMAGE.equals( nodeName ) ) { | |
| 102 | final var src = e.getAttribute( HTML_IMAGE_SRC ); | |
| 103 | final var ext = FilenameUtils.getExtension( src ); | |
| 104 | ||
| 105 | if( SVG_FILE.equalsIgnoreCase( ext ) ) { | |
| 106 | image = getCachedImage( | |
| 107 | src, svg -> rasterize( svg, box.getContentWidth() ) ); | |
| 108 | } | |
| 109 | } | |
| 110 | else if( HTML_TEX.equals( nodeName ) ) { | |
| 111 | // Convert the TeX element to a raster graphic if not yet cached. | |
| 112 | final var src = e.getTextContent(); | |
| 113 | image = getCachedImage( | |
| 114 | src, __ -> rasterize( getInstance().render( src ) ) | |
| 115 | ); | |
| 116 | } | |
| 117 | } catch( final Exception ex ) { | |
| 118 | alert( ex ); | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | if( image != null ) { | |
| 123 | final var w = image.getWidth( null ); | |
| 124 | final var h = image.getHeight( null ); | |
| 125 | ||
| 126 | return new ImageReplacedElement( image, w, h ); | |
| 127 | } | |
| 128 | ||
| 129 | return null; | |
| 130 | } | |
| 131 | ||
| 132 | @Override | |
| 133 | public void reset() { | |
| 134 | } | |
| 135 | ||
| 136 | @Override | |
| 137 | public void remove( final Element e ) { | |
| 138 | } | |
| 139 | ||
| 140 | @Override | |
| 141 | public void setFormSubmissionListener( FormSubmissionListener listener ) { | |
| 142 | } | |
| 143 | ||
| 144 | /** | |
| 145 | * Returns an image associated with a string; the string's pre-computed | |
| 146 | * hash code is returned as the string value, making this operation very | |
| 147 | * quick to return the corresponding {@link BufferedImage}. | |
| 148 | * | |
| 149 | * @param src The source used for the key into the image cache. | |
| 150 | * @param rasterizer {@link Function} to call to rasterize an image. | |
| 151 | * @return The image that corresponds to the given source string. | |
| 152 | */ | |
| 153 | private BufferedImage getCachedImage( | |
| 154 | final String src, final Function<String, BufferedImage> rasterizer ) { | |
| 155 | return mImageCache.computeIfAbsent( src, __ -> rasterizer.apply( src ) ); | |
| 156 | } | |
| 157 | } | |
| 1 | 158 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | /** | |
| 31 | * Responsible for transforming a document through a variety of chained | |
| 32 | * handlers. If there are conditions where this handler should not process the | |
| 33 | * entire chain, create a second handler, or split the chain into reusable | |
| 34 | * sub-chains. | |
| 35 | * | |
| 36 | * @param <T> The type of object to process. | |
| 37 | */ | |
| 38 | public abstract class AbstractProcessor<T> implements Processor<T> { | |
| 39 | ||
| 40 | /** | |
| 41 | * Used while processing the entire chain; null to signify no more links. | |
| 42 | */ | |
| 43 | private final Processor<T> mNext; | |
| 44 | ||
| 45 | /** | |
| 46 | * Constructs a new default handler with no successor. | |
| 47 | */ | |
| 48 | protected AbstractProcessor() { | |
| 49 | this( null ); | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Constructs a new default handler with a given successor. | |
| 54 | * | |
| 55 | * @param successor The next processor in the chain. | |
| 56 | */ | |
| 57 | public AbstractProcessor( final Processor<T> successor ) { | |
| 58 | mNext = successor; | |
| 59 | } | |
| 60 | ||
| 61 | @Override | |
| 62 | public Processor<T> next() { | |
| 63 | return mNext; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * This algorithm is incorrect, but works for the one use case of removing | |
| 68 | * the ending HTML Preview Processor from the end of the processor chain. | |
| 69 | * The processor chain is immutable so this creates a succession of | |
| 70 | * delegators that wrap each processor in the chain, except for the one | |
| 71 | * to be removed. | |
| 72 | * <p> | |
| 73 | * An alternative is to update the {@link ProcessorFactory} with the ability | |
| 74 | * to create a processor chain devoid of an {@link HtmlPreviewProcessor}. | |
| 75 | * </p> | |
| 76 | * | |
| 77 | * @param removal The {@link Processor} to remove from the chain. | |
| 78 | * @return A delegating processor chain starting from this processor | |
| 79 | * onwards with the given processor removed from the chain. | |
| 80 | */ | |
| 81 | @Override | |
| 82 | public Processor<T> remove( final Class<? extends Processor<T>> removal ) { | |
| 83 | Processor<T> p = this; | |
| 84 | final ProcessorDelegator<T> head = new ProcessorDelegator<>( p ); | |
| 85 | ProcessorDelegator<T> result = head; | |
| 86 | ||
| 87 | while( p != null ) { | |
| 88 | final Processor<T> next = p.next(); | |
| 89 | ||
| 90 | if( next != null && next.getClass() != removal ) { | |
| 91 | final var delegator = new ProcessorDelegator<>( next ); | |
| 92 | ||
| 93 | result.setNext( delegator ); | |
| 94 | result = delegator; | |
| 95 | } | |
| 96 | ||
| 97 | p = p.next(); | |
| 98 | } | |
| 99 | ||
| 100 | return head; | |
| 101 | } | |
| 102 | ||
| 103 | private static final class ProcessorDelegator<T> | |
| 104 | extends AbstractProcessor<T> { | |
| 105 | private final Processor<T> mDelegate; | |
| 106 | private Processor<T> mNext; | |
| 107 | ||
| 108 | public ProcessorDelegator( final Processor<T> delegate ) { | |
| 109 | super( delegate ); | |
| 110 | ||
| 111 | assert delegate != null; | |
| 112 | ||
| 113 | mDelegate = delegate; | |
| 114 | } | |
| 115 | ||
| 116 | @Override | |
| 117 | public T apply( T t ) { | |
| 118 | return mDelegate.apply( t ); | |
| 119 | } | |
| 120 | ||
| 121 | protected void setNext( final Processor<T> next ) { | |
| 122 | mNext = next; | |
| 123 | } | |
| 124 | ||
| 125 | @Override | |
| 126 | public Processor<T> next() { | |
| 127 | return mNext; | |
| 128 | } | |
| 129 | } | |
| 130 | } | |
| 1 | 131 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 33 | ||
| 34 | /** | |
| 35 | * Processes interpolated string definitions in the document and inserts | |
| 36 | * their values into the post-processed text. The default variable syntax is | |
| 37 | * {@code $variable$}. | |
| 38 | */ | |
| 39 | public class DefinitionProcessor extends AbstractProcessor<String> { | |
| 40 | ||
| 41 | private final Map<String, String> mDefinitions; | |
| 42 | ||
| 43 | public DefinitionProcessor( | |
| 44 | final Processor<String> successor, final Map<String, String> map ) { | |
| 45 | super( successor ); | |
| 46 | mDefinitions = map; | |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * Processes the given text document by replacing variables with their values. | |
| 51 | * | |
| 52 | * @param text The document text that includes variables that should be | |
| 53 | * replaced with values when rendered as HTML. | |
| 54 | * @return The text with all variables replaced. | |
| 55 | */ | |
| 56 | @Override | |
| 57 | public String apply( final String text ) { | |
| 58 | return replace( text, getDefinitions() ); | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Returns the map to use for variable substitution. | |
| 63 | * | |
| 64 | * @return A map of variable names to values. | |
| 65 | */ | |
| 66 | protected Map<String, String> getDefinitions() { | |
| 67 | return mDefinitions; | |
| 68 | } | |
| 69 | } | |
| 1 | 70 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for notifying the HTMLPreviewPane when the succession chain has | |
| 34 | * updated. This decouples knowledge of changes to the editor panel from the | |
| 35 | * HTML preview panel as well as any processing that takes place before the | |
| 36 | * final HTML preview is rendered. This should be the last link in the processor | |
| 37 | * chain. | |
| 38 | */ | |
| 39 | public class HtmlPreviewProcessor extends AbstractProcessor<String> { | |
| 40 | ||
| 41 | // There is only one preview panel. | |
| 42 | private static HTMLPreviewPane sHtmlPreviewPane; | |
| 43 | ||
| 44 | /** | |
| 45 | * Constructs the end of a processing chain. | |
| 46 | * | |
| 47 | * @param htmlPreviewPane The pane to update with the post-processed document. | |
| 48 | */ | |
| 49 | public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) { | |
| 50 | sHtmlPreviewPane = htmlPreviewPane; | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * Update the preview panel using HTML from the succession chain. | |
| 55 | * | |
| 56 | * @param html The document content to render in the preview pane. The HTML | |
| 57 | * should not contain a doctype, head, or body tag, only | |
| 58 | * content to render within the body. | |
| 59 | * @return {@code null} to indicate no more processors in the chain. | |
| 60 | */ | |
| 61 | @Override | |
| 62 | public String apply( final String html ) { | |
| 63 | getHtmlPreviewPane().process( html ); | |
| 64 | ||
| 65 | // No more processing required. | |
| 66 | return null; | |
| 67 | } | |
| 68 | ||
| 69 | private HTMLPreviewPane getHtmlPreviewPane() { | |
| 70 | return sHtmlPreviewPane; | |
| 71 | } | |
| 72 | } | |
| 1 | 73 |
| 1 | /* | |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | /** | |
| 31 | * This is the default processor used when an unknown filename extension is | |
| 32 | * encountered. | |
| 33 | */ | |
| 34 | public class IdentityProcessor extends AbstractProcessor<String> { | |
| 35 | ||
| 36 | /** | |
| 37 | * Passes the link to the super constructor. | |
| 38 | * | |
| 39 | * @param successor The next processor in the chain to use for text | |
| 40 | * processing. | |
| 41 | */ | |
| 42 | public IdentityProcessor( final Processor<String> successor ) { | |
| 43 | super( successor ); | |
| 44 | } | |
| 45 | ||
| 46 | /** | |
| 47 | * Returns the given string, modified with "pre" tags. | |
| 48 | * | |
| 49 | * @param t The string to return, enclosed in "pre" tags. | |
| 50 | * @return The value of t wrapped in "pre" tags. | |
| 51 | */ | |
| 52 | @Override | |
| 53 | public String apply( final String t ) { | |
| 54 | return "<pre>" + t + "</pre>"; | |
| 55 | } | |
| 56 | } | |
| 1 | 57 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | import com.keenwrite.preferences.UserPreferences; | |
| 31 | import javafx.beans.property.ObjectProperty; | |
| 32 | import javafx.beans.property.StringProperty; | |
| 33 | ||
| 34 | import javax.script.ScriptEngine; | |
| 35 | import javax.script.ScriptEngineManager; | |
| 36 | import java.io.File; | |
| 37 | import java.nio.file.Path; | |
| 38 | import java.util.LinkedHashMap; | |
| 39 | import java.util.Map; | |
| 40 | import java.util.concurrent.atomic.AtomicBoolean; | |
| 41 | ||
| 42 | import static com.keenwrite.Constants.STATUS_PARSE_ERROR; | |
| 43 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 44 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 45 | import static com.keenwrite.sigils.RSigilOperator.PREFIX; | |
| 46 | import static com.keenwrite.sigils.RSigilOperator.SUFFIX; | |
| 47 | import static java.lang.Math.min; | |
| 48 | ||
| 49 | /** | |
| 50 | * Transforms a document containing R statements into Markdown. | |
| 51 | */ | |
| 52 | public final class InlineRProcessor extends DefinitionProcessor { | |
| 53 | /** | |
| 54 | * Constrain memory when typing new R expressions into the document. | |
| 55 | */ | |
| 56 | private static final int MAX_CACHED_R_STATEMENTS = 512; | |
| 57 | ||
| 58 | /** | |
| 59 | * Where to put document inline evaluated R expressions. | |
| 60 | */ | |
| 61 | private final Map<String, Object> mEvalCache = new LinkedHashMap<>() { | |
| 62 | @Override | |
| 63 | protected boolean removeEldestEntry( | |
| 64 | final Map.Entry<String, Object> eldest ) { | |
| 65 | return size() > MAX_CACHED_R_STATEMENTS; | |
| 66 | } | |
| 67 | }; | |
| 68 | ||
| 69 | /** | |
| 70 | * Only one editor is open at a time. | |
| 71 | */ | |
| 72 | private static final ScriptEngine ENGINE = | |
| 73 | (new ScriptEngineManager()).getEngineByName( "Renjin" ); | |
| 74 | ||
| 75 | private static final int PREFIX_LENGTH = PREFIX.length(); | |
| 76 | ||
| 77 | private final AtomicBoolean mDirty = new AtomicBoolean( false ); | |
| 78 | ||
| 79 | /** | |
| 80 | * Constructs a processor capable of evaluating R statements. | |
| 81 | * | |
| 82 | * @param successor Subsequent link in the processing chain. | |
| 83 | * @param map Resolved definitions map. | |
| 84 | */ | |
| 85 | public InlineRProcessor( | |
| 86 | final Processor<String> successor, | |
| 87 | final Map<String, String> map ) { | |
| 88 | super( successor, map ); | |
| 89 | ||
| 90 | bootstrapScriptProperty().addListener( | |
| 91 | ( ob, oldScript, newScript ) -> setDirty( true ) ); | |
| 92 | workingDirectoryProperty().addListener( | |
| 93 | ( ob, oldScript, newScript ) -> setDirty( true ) ); | |
| 94 | ||
| 95 | getUserPreferences().addSaveEventHandler( ( handler ) -> { | |
| 96 | if( isDirty() ) { | |
| 97 | init(); | |
| 98 | setDirty( false ); | |
| 99 | } | |
| 100 | } ); | |
| 101 | ||
| 102 | init(); | |
| 103 | } | |
| 104 | ||
| 105 | /** | |
| 106 | * Initialises the R code so that R can find imported libraries. Note that | |
| 107 | * any existing R functionality will not be overwritten if this method is | |
| 108 | * called multiple times. | |
| 109 | */ | |
| 110 | private void init() { | |
| 111 | final var bootstrap = getBootstrapScript(); | |
| 112 | ||
| 113 | if( !bootstrap.isBlank() ) { | |
| 114 | final var wd = getWorkingDirectory(); | |
| 115 | final var dir = wd.toString().replace( '\\', '/' ); | |
| 116 | final var map = getDefinitions(); | |
| 117 | map.put( "$application.r.working.directory$", dir ); | |
| 118 | ||
| 119 | eval( replace( bootstrap, map ) ); | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | /** | |
| 124 | * Sets the dirty flag to indicate that the bootstrap script or working | |
| 125 | * directory has been modified. Upon saving the preferences, if this flag | |
| 126 | * is true, then {@link #init()} will be called to reload the R environment. | |
| 127 | * | |
| 128 | * @param dirty Set to true to reload changes upon closing preferences. | |
| 129 | */ | |
| 130 | private void setDirty( final boolean dirty ) { | |
| 131 | mDirty.set( dirty ); | |
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * Answers whether R-related settings have been modified. | |
| 136 | * | |
| 137 | * @return {@code true} when the settings have changed. | |
| 138 | */ | |
| 139 | private boolean isDirty() { | |
| 140 | return mDirty.get(); | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Evaluates all R statements in the source document and inserts the | |
| 145 | * calculated value into the generated document. | |
| 146 | * | |
| 147 | * @param text The document text that includes variables that should be | |
| 148 | * replaced with values when rendered as HTML. | |
| 149 | * @return The generated document with output from all R statements | |
| 150 | * substituted with value returned from their execution. | |
| 151 | */ | |
| 152 | @Override | |
| 153 | public String apply( final String text ) { | |
| 154 | final int length = text.length(); | |
| 155 | ||
| 156 | // The * 2 is a wild guess at the ratio of R statements to the length | |
| 157 | // of text produced by those statements. | |
| 158 | final StringBuilder sb = new StringBuilder( length * 2 ); | |
| 159 | ||
| 160 | int prevIndex = 0; | |
| 161 | int currIndex = text.indexOf( PREFIX ); | |
| 162 | ||
| 163 | while( currIndex >= 0 ) { | |
| 164 | // Copy everything up to, but not including, an R statement (`r#). | |
| 165 | sb.append( text, prevIndex, currIndex ); | |
| 166 | ||
| 167 | // Jump to the start of the R statement. | |
| 168 | prevIndex = currIndex + PREFIX_LENGTH; | |
| 169 | ||
| 170 | // Find the statement ending (`), without indexing past the text boundary. | |
| 171 | currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) ); | |
| 172 | ||
| 173 | // Only evaluate inline R statements that have end delimiters. | |
| 174 | if( currIndex > 1 ) { | |
| 175 | // Extract the inline R statement to be evaluated. | |
| 176 | final String r = text.substring( prevIndex, currIndex ); | |
| 177 | ||
| 178 | // Pass the R statement into the R engine for evaluation. | |
| 179 | try { | |
| 180 | final Object result = evalText( r ); | |
| 181 | ||
| 182 | // Append the string representation of the result into the text. | |
| 183 | sb.append( result ); | |
| 184 | } catch( final Exception e ) { | |
| 185 | // If the string couldn't be parsed using R, append the statement | |
| 186 | // that failed to parse, instead of its evaluated value. | |
| 187 | sb.append( PREFIX ).append( r ).append( SUFFIX ); | |
| 188 | ||
| 189 | // Tell the user that there was a problem. | |
| 190 | alert( STATUS_PARSE_ERROR, e.getMessage(), currIndex ); | |
| 191 | } | |
| 192 | ||
| 193 | // Retain the R statement's ending position in the text. | |
| 194 | prevIndex = currIndex + 1; | |
| 195 | } | |
| 196 | ||
| 197 | // Find the start of the next inline R statement. | |
| 198 | currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) ); | |
| 199 | } | |
| 200 | ||
| 201 | // Copy from the previous index to the end of the string. | |
| 202 | return sb.append( text.substring( min( prevIndex, length ) ) ).toString(); | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * Look up an R expression from the cache then return the resulting object. | |
| 207 | * If the R expression hasn't been cached, it'll first be evaluated. | |
| 208 | * | |
| 209 | * @param r The expression to evaluate. | |
| 210 | * @return The object resulting from the evaluation. | |
| 211 | */ | |
| 212 | private Object evalText( final String r ) { | |
| 213 | return mEvalCache.computeIfAbsent( r, v -> eval( r ) ); | |
| 214 | } | |
| 215 | ||
| 216 | /** | |
| 217 | * Evaluate an R expression and return the resulting object. | |
| 218 | * | |
| 219 | * @param r The expression to evaluate. | |
| 220 | * @return The object resulting from the evaluation. | |
| 221 | */ | |
| 222 | private Object eval( final String r ) { | |
| 223 | try { | |
| 224 | return getScriptEngine().eval( r ); | |
| 225 | } catch( final Exception ex ) { | |
| 226 | final String expr = r.substring( 0, min( r.length(), 30 ) ); | |
| 227 | alert( "Main.status.error.r", expr, ex.getMessage() ); | |
| 228 | } | |
| 229 | ||
| 230 | return ""; | |
| 231 | } | |
| 232 | ||
| 233 | /** | |
| 234 | * Return the given path if not {@code null}, otherwise return the path to | |
| 235 | * the user's directory. | |
| 236 | * | |
| 237 | * @return A non-null path. | |
| 238 | */ | |
| 239 | private Path getWorkingDirectory() { | |
| 240 | return getUserPreferences().getRDirectory().toPath(); | |
| 241 | } | |
| 242 | ||
| 243 | private ObjectProperty<File> workingDirectoryProperty() { | |
| 244 | return getUserPreferences().rDirectoryProperty(); | |
| 245 | } | |
| 246 | ||
| 247 | /** | |
| 248 | * Loads the R init script from the application's persisted preferences. | |
| 249 | * | |
| 250 | * @return A non-null string, possibly empty. | |
| 251 | */ | |
| 252 | private String getBootstrapScript() { | |
| 253 | return getUserPreferences().getRScript(); | |
| 254 | } | |
| 255 | ||
| 256 | private StringProperty bootstrapScriptProperty() { | |
| 257 | return getUserPreferences().rScriptProperty(); | |
| 258 | } | |
| 259 | ||
| 260 | private UserPreferences getUserPreferences() { | |
| 261 | return UserPreferences.getInstance(); | |
| 262 | } | |
| 263 | ||
| 264 | private ScriptEngine getScriptEngine() { | |
| 265 | return ENGINE; | |
| 266 | } | |
| 267 | } | |
| 1 | 268 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | import java.util.function.UnaryOperator; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for processing documents from one known format to another. | |
| 34 | * Processes the given content providing a transformation from one document | |
| 35 | * format into another. For example, this could convert from XML to text using | |
| 36 | * an XSLT processor, or from markdown to HTML. | |
| 37 | * | |
| 38 | * @param <T> The type of processor to create. | |
| 39 | */ | |
| 40 | public interface Processor<T> extends UnaryOperator<T> { | |
| 41 | ||
| 42 | /** | |
| 43 | * Removes the given processor from the chain, returning a new immutable | |
| 44 | * chain equivalent to this chain, but without the given processor. | |
| 45 | * | |
| 46 | * @param processor The {@link Processor} to remove from the chain. | |
| 47 | * @return A delegating processor chain starting from this processor | |
| 48 | * onwards with the given processor removed from the chain. | |
| 49 | */ | |
| 50 | Processor<T> remove( Class<? extends Processor<T>> processor ); | |
| 51 | ||
| 52 | /** | |
| 53 | * Adds a document processor to call after this processor finishes processing | |
| 54 | * the document given to the process method. | |
| 55 | * | |
| 56 | * @return The processor that should transform the document after this | |
| 57 | * instance has finished processing, or {@code null} if this is the last | |
| 58 | * processor in the chain. | |
| 59 | */ | |
| 60 | default Processor<T> next() { | |
| 61 | return null; | |
| 62 | } | |
| 63 | } | |
| 1 | 64 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | import com.keenwrite.AbstractFileFactory; | |
| 31 | import com.keenwrite.FileEditorTab; | |
| 32 | import com.keenwrite.preview.HTMLPreviewPane; | |
| 33 | import com.keenwrite.processors.markdown.MarkdownProcessor; | |
| 34 | ||
| 35 | import java.util.Map; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for creating processors capable of parsing, transforming, | |
| 39 | * interpolating, and rendering known file types. | |
| 40 | */ | |
| 41 | public class ProcessorFactory extends AbstractFileFactory { | |
| 42 | ||
| 43 | private final HTMLPreviewPane mPreviewPane; | |
| 44 | private final Map<String, String> mResolvedMap; | |
| 45 | private final Processor<String> mMarkdownProcessor; | |
| 46 | ||
| 47 | /** | |
| 48 | * Constructs a factory with the ability to create processors that can perform | |
| 49 | * text and caret processing to generate a final preview. | |
| 50 | * | |
| 51 | * @param previewPane Where the final output is rendered. | |
| 52 | * @param resolvedMap Flat map of definitions to replace before final render. | |
| 53 | */ | |
| 54 | public ProcessorFactory( | |
| 55 | final HTMLPreviewPane previewPane, | |
| 56 | final Map<String, String> resolvedMap ) { | |
| 57 | mPreviewPane = previewPane; | |
| 58 | mResolvedMap = resolvedMap; | |
| 59 | mMarkdownProcessor = createMarkdownProcessor(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Creates a processor chain suitable for parsing and rendering the file | |
| 64 | * opened at the given tab. | |
| 65 | * | |
| 66 | * @param tab The tab containing a text editor, path, and caret position. | |
| 67 | * @return A processor that can render the given tab's text. | |
| 68 | */ | |
| 69 | public Processor<String> createProcessors( final FileEditorTab tab ) { | |
| 70 | return switch( lookup( tab.getPath() ) ) { | |
| 71 | case RMARKDOWN -> createRProcessor(); | |
| 72 | case SOURCE -> createMarkdownDefinitionProcessor(); | |
| 73 | case XML -> createXMLProcessor( tab ); | |
| 74 | case RXML -> createRXMLProcessor( tab ); | |
| 75 | default -> createIdentityProcessor(); | |
| 76 | }; | |
| 77 | } | |
| 78 | ||
| 79 | private Processor<String> createHTMLPreviewProcessor() { | |
| 80 | return new HtmlPreviewProcessor( getPreviewPane() ); | |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Creates and links the processors at the end of the processing chain. | |
| 85 | * | |
| 86 | * @return A markdown, caret replacement, and preview pane processor chain. | |
| 87 | */ | |
| 88 | private Processor<String> createMarkdownProcessor() { | |
| 89 | final var hpp = createHTMLPreviewProcessor(); | |
| 90 | return new MarkdownProcessor( hpp, getPreviewPane().getPath() ); | |
| 91 | } | |
| 92 | ||
| 93 | protected Processor<String> createIdentityProcessor() { | |
| 94 | final var hpp = createHTMLPreviewProcessor(); | |
| 95 | return new IdentityProcessor( hpp ); | |
| 96 | } | |
| 97 | ||
| 98 | protected Processor<String> createDefinitionProcessor( | |
| 99 | final Processor<String> p ) { | |
| 100 | return new DefinitionProcessor( p, getResolvedMap() ); | |
| 101 | } | |
| 102 | ||
| 103 | protected Processor<String> createMarkdownDefinitionProcessor() { | |
| 104 | final var tpc = getCommonProcessor(); | |
| 105 | return createDefinitionProcessor( tpc ); | |
| 106 | } | |
| 107 | ||
| 108 | protected Processor<String> createXMLProcessor( final FileEditorTab tab ) { | |
| 109 | final var tpc = getCommonProcessor(); | |
| 110 | final var xmlp = new XmlProcessor( tpc, tab.getPath() ); | |
| 111 | return createDefinitionProcessor( xmlp ); | |
| 112 | } | |
| 113 | ||
| 114 | protected Processor<String> createRProcessor() { | |
| 115 | final var tpc = getCommonProcessor(); | |
| 116 | final var rp = new InlineRProcessor( tpc, getResolvedMap() ); | |
| 117 | return new RVariableProcessor( rp, getResolvedMap() ); | |
| 118 | } | |
| 119 | ||
| 120 | protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) { | |
| 121 | final var tpc = getCommonProcessor(); | |
| 122 | final var xmlp = new XmlProcessor( tpc, tab.getPath() ); | |
| 123 | final var rp = new InlineRProcessor( xmlp, getResolvedMap() ); | |
| 124 | return new RVariableProcessor( rp, getResolvedMap() ); | |
| 125 | } | |
| 126 | ||
| 127 | private HTMLPreviewPane getPreviewPane() { | |
| 128 | return mPreviewPane; | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * Returns the variable map of interpolated definitions. | |
| 133 | * | |
| 134 | * @return A map to help dereference variables. | |
| 135 | */ | |
| 136 | private Map<String, String> getResolvedMap() { | |
| 137 | return mResolvedMap; | |
| 138 | } | |
| 139 | ||
| 140 | /** | |
| 141 | * Returns a processor common to all processors: markdown, caret position | |
| 142 | * token replacer, and an HTML preview renderer. | |
| 143 | * | |
| 144 | * @return Processors at the end of the processing chain. | |
| 145 | */ | |
| 146 | private Processor<String> getCommonProcessor() { | |
| 147 | return mMarkdownProcessor; | |
| 148 | } | |
| 149 | } | |
| 1 | 150 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | import com.keenwrite.sigils.RSigilOperator; | |
| 31 | ||
| 32 | import java.util.HashMap; | |
| 33 | import java.util.Map; | |
| 34 | ||
| 35 | /** | |
| 36 | * Converts the keys of the resolved map from default form to R form, then | |
| 37 | * performs a substitution on the text. The default R variable syntax is | |
| 38 | * {@code v$tree$leaf}. | |
| 39 | */ | |
| 40 | public class RVariableProcessor extends DefinitionProcessor { | |
| 41 | ||
| 42 | public RVariableProcessor( | |
| 43 | final Processor<String> rp, final Map<String, String> map ) { | |
| 44 | super( rp, map ); | |
| 45 | } | |
| 46 | ||
| 47 | /** | |
| 48 | * Returns the R-based version of the interpolated variable definitions. | |
| 49 | * | |
| 50 | * @return Variable names transmogrified from the default syntax to R syntax. | |
| 51 | */ | |
| 52 | @Override | |
| 53 | protected Map<String, String> getDefinitions() { | |
| 54 | return toR( super.getDefinitions() ); | |
| 55 | } | |
| 56 | ||
| 57 | /** | |
| 58 | * Converts the given map from regular variables to R variables. | |
| 59 | * | |
| 60 | * @param map Map of variable names to values. | |
| 61 | * @return Map of R variables. | |
| 62 | */ | |
| 63 | private Map<String, String> toR( final Map<String, String> map ) { | |
| 64 | final var rMap = new HashMap<String, String>( map.size() ); | |
| 65 | ||
| 66 | for( final var entry : map.entrySet() ) { | |
| 67 | final var key = entry.getKey(); | |
| 68 | rMap.put( RSigilOperator.entoken( key ), toRValue( map.get( key ) ) ); | |
| 69 | } | |
| 70 | ||
| 71 | return rMap; | |
| 72 | } | |
| 73 | ||
| 74 | private String toRValue( final String value ) { | |
| 75 | return '\'' + escape( value, '\'', "\\'" ) + '\''; | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * TODO: Make generic method for replacing text. | |
| 80 | * | |
| 81 | * @param haystack Search this string for the needle, must not be null. | |
| 82 | * @param needle The character to find in the haystack. | |
| 83 | * @param thread Replace the needle with this text, if the needle is found. | |
| 84 | * @return The haystack with the all instances of needle replaced with thread. | |
| 85 | */ | |
| 86 | @SuppressWarnings("SameParameterValue") | |
| 87 | private String escape( | |
| 88 | final String haystack, final char needle, final String thread ) { | |
| 89 | int end = haystack.indexOf( needle ); | |
| 90 | ||
| 91 | if( end < 0 ) { | |
| 92 | return haystack; | |
| 93 | } | |
| 94 | ||
| 95 | final int length = haystack.length(); | |
| 96 | int start = 0; | |
| 97 | ||
| 98 | // Replace up to 32 occurrences before the string reallocates its buffer. | |
| 99 | final StringBuilder sb = new StringBuilder( length + 32 ); | |
| 100 | ||
| 101 | while( end >= 0 ) { | |
| 102 | sb.append( haystack, start, end ).append( thread ); | |
| 103 | start = end + 1; | |
| 104 | end = haystack.indexOf( needle, start ); | |
| 105 | } | |
| 106 | ||
| 107 | return sb.append( haystack.substring( start ) ).toString(); | |
| 108 | } | |
| 109 | } | |
| 1 | 110 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors; | |
| 29 | ||
| 30 | import com.keenwrite.Services; | |
| 31 | import com.keenwrite.service.Snitch; | |
| 32 | import net.sf.saxon.TransformerFactoryImpl; | |
| 33 | import net.sf.saxon.trans.XPathException; | |
| 34 | ||
| 35 | import javax.xml.stream.XMLEventReader; | |
| 36 | import javax.xml.stream.XMLInputFactory; | |
| 37 | import javax.xml.stream.XMLStreamException; | |
| 38 | import javax.xml.stream.events.ProcessingInstruction; | |
| 39 | import javax.xml.stream.events.XMLEvent; | |
| 40 | import javax.xml.transform.*; | |
| 41 | import javax.xml.transform.stream.StreamResult; | |
| 42 | import javax.xml.transform.stream.StreamSource; | |
| 43 | import java.io.File; | |
| 44 | import java.io.Reader; | |
| 45 | import java.io.StringReader; | |
| 46 | import java.io.StringWriter; | |
| 47 | import java.nio.file.Path; | |
| 48 | import java.nio.file.Paths; | |
| 49 | ||
| 50 | import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute; | |
| 51 | ||
| 52 | /** | |
| 53 | * Transforms an XML document. The XML document must have a stylesheet specified | |
| 54 | * as part of its processing instructions, such as: | |
| 55 | * <p> | |
| 56 | * {@code xml-stylesheet type="text/xsl" href="markdown.xsl"} | |
| 57 | * </p> | |
| 58 | * <p> | |
| 59 | * The XSL must transform the XML document into Markdown, or another format | |
| 60 | * recognized by the next link on the chain. | |
| 61 | * </p> | |
| 62 | */ | |
| 63 | public class XmlProcessor extends AbstractProcessor<String> | |
| 64 | implements ErrorListener { | |
| 65 | ||
| 66 | private final Snitch snitch = Services.load( Snitch.class ); | |
| 67 | ||
| 68 | private XMLInputFactory xmlInputFactory; | |
| 69 | private TransformerFactory transformerFactory; | |
| 70 | private Transformer transformer; | |
| 71 | ||
| 72 | private Path path; | |
| 73 | ||
| 74 | /** | |
| 75 | * Constructs an XML processor that can transform an XML document into another | |
| 76 | * format based on the XSL file specified as a processing instruction. The | |
| 77 | * path must point to the directory where the XSL file is found, which implies | |
| 78 | * that they must be in the same directory. | |
| 79 | * | |
| 80 | * @param processor Next link in the processing chain. | |
| 81 | * @param path The path to the XML file content to be processed. | |
| 82 | */ | |
| 83 | public XmlProcessor( final Processor<String> processor, final Path path ) { | |
| 84 | super( processor ); | |
| 85 | setPath( path ); | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Transforms the given XML text into another form (typically Markdown). | |
| 90 | * | |
| 91 | * @param text The text to transform, can be empty, cannot be null. | |
| 92 | * @return The transformed text, or empty if text is empty. | |
| 93 | */ | |
| 94 | @Override | |
| 95 | public String apply( final String text ) { | |
| 96 | try { | |
| 97 | return text.isEmpty() ? text : transform( text ); | |
| 98 | } catch( final Exception ex ) { | |
| 99 | throw new RuntimeException( ex ); | |
| 100 | } | |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * Performs an XSL transformation on the given XML text. The XML text must | |
| 105 | * have a processing instruction that points to the XSL template file to use | |
| 106 | * for the transformation. | |
| 107 | * | |
| 108 | * @param text The text to transform. | |
| 109 | * @return The transformed text. | |
| 110 | */ | |
| 111 | private String transform( final String text ) throws Exception { | |
| 112 | // Extract the XML stylesheet processing instruction. | |
| 113 | final String template = getXsltFilename( text ); | |
| 114 | final Path xsl = getXslPath( template ); | |
| 115 | ||
| 116 | try( | |
| 117 | final StringWriter output = new StringWriter( text.length() ); | |
| 118 | final StringReader input = new StringReader( text ) ) { | |
| 119 | ||
| 120 | // Listen for external file modification events. | |
| 121 | getSnitch().listen( xsl ); | |
| 122 | ||
| 123 | getTransformer( xsl ).transform( | |
| 124 | new StreamSource( input ), | |
| 125 | new StreamResult( output ) | |
| 126 | ); | |
| 127 | ||
| 128 | return output.toString(); | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * Returns an XSL transformer ready to transform an XML document using the | |
| 134 | * XSLT file specified by the given path. If the path is already known then | |
| 135 | * this will return the associated transformer. | |
| 136 | * | |
| 137 | * @param xsl The path to an XSLT file. | |
| 138 | * @return A transformer that will transform XML documents using the given | |
| 139 | * XSLT file. | |
| 140 | * @throws TransformerConfigurationException Could not instantiate the | |
| 141 | * transformer. | |
| 142 | */ | |
| 143 | private Transformer getTransformer( final Path xsl ) | |
| 144 | throws TransformerConfigurationException { | |
| 145 | if( this.transformer == null ) { | |
| 146 | this.transformer = createTransformer( xsl ); | |
| 147 | } | |
| 148 | ||
| 149 | return this.transformer; | |
| 150 | } | |
| 151 | ||
| 152 | /** | |
| 153 | * Creates a configured transformer ready to run. | |
| 154 | * | |
| 155 | * @param xsl The stylesheet to use for transforming XML documents. | |
| 156 | * @return The edited XML document transformed into another format (usually | |
| 157 | * markdown). | |
| 158 | * @throws TransformerConfigurationException Could not create the transformer. | |
| 159 | */ | |
| 160 | protected Transformer createTransformer( final Path xsl ) | |
| 161 | throws TransformerConfigurationException { | |
| 162 | final Source xslt = new StreamSource( xsl.toFile() ); | |
| 163 | ||
| 164 | return getTransformerFactory().newTransformer( xslt ); | |
| 165 | } | |
| 166 | ||
| 167 | private Path getXslPath( final String filename ) { | |
| 168 | final Path xmlPath = getPath(); | |
| 169 | final File xmlDirectory = xmlPath.toFile().getParentFile(); | |
| 170 | ||
| 171 | return Paths.get( xmlDirectory.getPath(), filename ); | |
| 172 | } | |
| 173 | ||
| 174 | /** | |
| 175 | * Given XML text, this will use a StAX pull reader to obtain the XML | |
| 176 | * stylesheet processing instruction. This will throw a parse exception if the | |
| 177 | * href pseudo-attribute filename value cannot be found. | |
| 178 | * | |
| 179 | * @param xml The XML containing an xml-stylesheet processing instruction. | |
| 180 | * @return The href pseudo-attribute value. | |
| 181 | * @throws XMLStreamException Could not parse the XML file. | |
| 182 | */ | |
| 183 | private String getXsltFilename( final String xml ) | |
| 184 | throws XMLStreamException, XPathException { | |
| 185 | ||
| 186 | String result = ""; | |
| 187 | ||
| 188 | try( final StringReader sr = new StringReader( xml ) ) { | |
| 189 | boolean found = false; | |
| 190 | int count = 0; | |
| 191 | final XMLEventReader reader = createXMLEventReader( sr ); | |
| 192 | ||
| 193 | // If the processing instruction wasn't found in the first 10 lines, | |
| 194 | // fail fast. This should iterate twice through the loop. | |
| 195 | while( !found && reader.hasNext() && count++ < 10 ) { | |
| 196 | final XMLEvent event = reader.nextEvent(); | |
| 197 | ||
| 198 | if( event.isProcessingInstruction() ) { | |
| 199 | final ProcessingInstruction pi = (ProcessingInstruction) event; | |
| 200 | final String target = pi.getTarget(); | |
| 201 | ||
| 202 | if( "xml-stylesheet".equalsIgnoreCase( target ) ) { | |
| 203 | result = getPseudoAttribute( pi.getData(), "href" ); | |
| 204 | found = true; | |
| 205 | } | |
| 206 | } | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | return result; | |
| 211 | } | |
| 212 | ||
| 213 | private XMLEventReader createXMLEventReader( final Reader reader ) | |
| 214 | throws XMLStreamException { | |
| 215 | return getXMLInputFactory().createXMLEventReader( reader ); | |
| 216 | } | |
| 217 | ||
| 218 | private synchronized XMLInputFactory getXMLInputFactory() { | |
| 219 | if( this.xmlInputFactory == null ) { | |
| 220 | this.xmlInputFactory = createXMLInputFactory(); | |
| 221 | } | |
| 222 | ||
| 223 | return this.xmlInputFactory; | |
| 224 | } | |
| 225 | ||
| 226 | private XMLInputFactory createXMLInputFactory() { | |
| 227 | return XMLInputFactory.newInstance(); | |
| 228 | } | |
| 229 | ||
| 230 | private synchronized TransformerFactory getTransformerFactory() { | |
| 231 | if( this.transformerFactory == null ) { | |
| 232 | this.transformerFactory = createTransformerFactory(); | |
| 233 | } | |
| 234 | ||
| 235 | return this.transformerFactory; | |
| 236 | } | |
| 237 | ||
| 238 | /** | |
| 239 | * Returns a high-performance XSLT 2 transformation engine. | |
| 240 | * | |
| 241 | * @return An XSL transforming engine. | |
| 242 | */ | |
| 243 | private TransformerFactory createTransformerFactory() { | |
| 244 | final TransformerFactory factory = new TransformerFactoryImpl(); | |
| 245 | ||
| 246 | // Bubble problems up to the user interface, rather than standard error. | |
| 247 | factory.setErrorListener( this ); | |
| 248 | ||
| 249 | return factory; | |
| 250 | } | |
| 251 | ||
| 252 | /** | |
| 253 | * Called when the XSL transformer issues a warning. | |
| 254 | * | |
| 255 | * @param ex The problem the transformer encountered. | |
| 256 | */ | |
| 257 | @Override | |
| 258 | public void warning( final TransformerException ex ) { | |
| 259 | throw new RuntimeException( ex ); | |
| 260 | } | |
| 261 | ||
| 262 | /** | |
| 263 | * Called when the XSL transformer issues an error. | |
| 264 | * | |
| 265 | * @param ex The problem the transformer encountered. | |
| 266 | */ | |
| 267 | @Override | |
| 268 | public void error( final TransformerException ex ) { | |
| 269 | throw new RuntimeException( ex ); | |
| 270 | } | |
| 271 | ||
| 272 | /** | |
| 273 | * Called when the XSL transformer issues a fatal error, which is probably | |
| 274 | * a bit over-dramatic a method name. | |
| 275 | * | |
| 276 | * @param ex The problem the transformer encountered. | |
| 277 | */ | |
| 278 | @Override | |
| 279 | public void fatalError( final TransformerException ex ) { | |
| 280 | throw new RuntimeException( ex ); | |
| 281 | } | |
| 282 | ||
| 283 | private void setPath( final Path path ) { | |
| 284 | this.path = path; | |
| 285 | } | |
| 286 | ||
| 287 | private Path getPath() { | |
| 288 | return this.path; | |
| 289 | } | |
| 290 | ||
| 291 | private Snitch getSnitch() { | |
| 292 | return this.snitch; | |
| 293 | } | |
| 294 | } | |
| 1 | 295 |
| 1 | package com.keenwrite.processors.markdown; | |
| 2 | ||
| 3 | import com.vladsch.flexmark.ast.BlockQuote; | |
| 4 | import com.vladsch.flexmark.ast.ListBlock; | |
| 5 | import com.vladsch.flexmark.html.AttributeProvider; | |
| 6 | import com.vladsch.flexmark.html.AttributeProviderFactory; | |
| 7 | import com.vladsch.flexmark.html.IndependentAttributeProviderFactory; | |
| 8 | import com.vladsch.flexmark.html.renderer.AttributablePart; | |
| 9 | import com.vladsch.flexmark.html.renderer.LinkResolverContext; | |
| 10 | import com.vladsch.flexmark.util.ast.Block; | |
| 11 | import com.vladsch.flexmark.util.ast.Node; | |
| 12 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 13 | import com.vladsch.flexmark.util.html.MutableAttributes; | |
| 14 | import org.jetbrains.annotations.NotNull; | |
| 15 | ||
| 16 | import static com.keenwrite.Constants.PARAGRAPH_ID_PREFIX; | |
| 17 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | |
| 18 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 19 | import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT; | |
| 20 | ||
| 21 | /** | |
| 22 | * Responsible for giving most block-level elements a unique identifier | |
| 23 | * attribute. The identifier is used to coordinate scrolling. | |
| 24 | */ | |
| 25 | public class BlockExtension implements HtmlRendererExtension { | |
| 26 | /** | |
| 27 | * Responsible for creating the id attribute. This class is instantiated | |
| 28 | * each time the document is rendered, thereby resetting the count to zero. | |
| 29 | */ | |
| 30 | public static class IdAttributeProvider implements AttributeProvider { | |
| 31 | private int mCount; | |
| 32 | ||
| 33 | private static AttributeProviderFactory createFactory() { | |
| 34 | return new IndependentAttributeProviderFactory() { | |
| 35 | @Override | |
| 36 | public @NotNull AttributeProvider apply( | |
| 37 | @NotNull final LinkResolverContext context ) { | |
| 38 | return new IdAttributeProvider(); | |
| 39 | } | |
| 40 | }; | |
| 41 | } | |
| 42 | ||
| 43 | @Override | |
| 44 | public void setAttributes( @NotNull Node node, | |
| 45 | @NotNull AttributablePart part, | |
| 46 | @NotNull MutableAttributes attributes ) { | |
| 47 | // Blockquotes are troublesome because they can interleave blank lines | |
| 48 | // without having an equivalent blank line in the source document. That | |
| 49 | // is, in Markdown the > symbol on a line by itself will generate a blank | |
| 50 | // line in the resulting document; however, a > symbol in the text editor | |
| 51 | // does not count as a blank line. Resolving this issue is tricky. | |
| 52 | // | |
| 53 | // The CODE_CONTENT represents <code> embedded inside <pre>; both elements | |
| 54 | // enter this method as FencedCodeBlock, but only the <pre> must be | |
| 55 | // uniquely identified (because they are the same line in Markdown). | |
| 56 | // | |
| 57 | if( node instanceof Block && | |
| 58 | !(node instanceof BlockQuote) && | |
| 59 | !(node instanceof ListBlock) && | |
| 60 | (part != CODE_CONTENT) ) { | |
| 61 | attributes.addValue( "id", PARAGRAPH_ID_PREFIX + mCount++ ); | |
| 62 | } | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | private BlockExtension() { | |
| 67 | } | |
| 68 | ||
| 69 | @Override | |
| 70 | public void extend( final Builder builder, | |
| 71 | @NotNull final String rendererType ) { | |
| 72 | builder.attributeProviderFactory( IdAttributeProvider.createFactory() ); | |
| 73 | } | |
| 74 | ||
| 75 | public static BlockExtension create() { | |
| 76 | return new BlockExtension(); | |
| 77 | } | |
| 78 | ||
| 79 | @Override | |
| 80 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 81 | } | |
| 82 | } | |
| 1 | 83 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.markdown; | |
| 29 | ||
| 30 | import com.keenwrite.exceptions.MissingFileException; | |
| 31 | import com.keenwrite.preferences.UserPreferences; | |
| 32 | import com.vladsch.flexmark.ast.Image; | |
| 33 | import com.vladsch.flexmark.html.IndependentLinkResolverFactory; | |
| 34 | import com.vladsch.flexmark.html.LinkResolver; | |
| 35 | import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext; | |
| 36 | import com.vladsch.flexmark.html.renderer.LinkStatus; | |
| 37 | import com.vladsch.flexmark.html.renderer.ResolvedLink; | |
| 38 | import com.vladsch.flexmark.util.ast.Node; | |
| 39 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 40 | import org.jetbrains.annotations.NotNull; | |
| 41 | import org.renjin.repackaged.guava.base.Splitter; | |
| 42 | ||
| 43 | import java.io.File; | |
| 44 | import java.nio.file.Path; | |
| 45 | ||
| 46 | import static com.keenwrite.StatusBarNotifier.alert; | |
| 47 | import static com.keenwrite.util.ProtocolResolver.getProtocol; | |
| 48 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | |
| 49 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 50 | import static java.lang.String.format; | |
| 51 | import static org.apache.commons.io.FilenameUtils.getExtension; | |
| 52 | import static org.apache.commons.io.FilenameUtils.removeExtension; | |
| 53 | ||
| 54 | /** | |
| 55 | * Responsible for ensuring that images can be rendered relative to a path. | |
| 56 | * This allows images to be located virtually anywhere. | |
| 57 | */ | |
| 58 | public class ImageLinkExtension implements HtmlRendererExtension { | |
| 59 | ||
| 60 | /** | |
| 61 | * Creates an extension capable of using a relative path to embed images. | |
| 62 | * | |
| 63 | * @param path The {@link Path} to the file being edited; the parent path | |
| 64 | * is the starting location of the relative image directory. | |
| 65 | * @return The new {@link ImageLinkExtension}, never {@code null}. | |
| 66 | */ | |
| 67 | public static ImageLinkExtension create( @NotNull final Path path ) { | |
| 68 | return new ImageLinkExtension( path ); | |
| 69 | } | |
| 70 | ||
| 71 | private class Factory extends IndependentLinkResolverFactory { | |
| 72 | @Override | |
| 73 | public @NotNull LinkResolver apply( | |
| 74 | @NotNull final LinkResolverBasicContext context ) { | |
| 75 | return new ImageLinkResolver(); | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | private class ImageLinkResolver implements LinkResolver { | |
| 80 | private final UserPreferences mUserPref = getUserPreferences(); | |
| 81 | private final File mImagesUserPrefix = mUserPref.getImagesDirectory(); | |
| 82 | private final String mImageExtensions = mUserPref.getImagesOrder(); | |
| 83 | ||
| 84 | public ImageLinkResolver() { | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * You can also set/clear/modify attributes through | |
| 89 | * {@link ResolvedLink#getAttributes()} and | |
| 90 | * {@link ResolvedLink#getNonNullAttributes()}. | |
| 91 | */ | |
| 92 | @NotNull | |
| 93 | @Override | |
| 94 | public ResolvedLink resolveLink( | |
| 95 | @NotNull final Node node, | |
| 96 | @NotNull final LinkResolverBasicContext context, | |
| 97 | @NotNull final ResolvedLink link ) { | |
| 98 | return node instanceof Image ? resolve( link ) : link; | |
| 99 | } | |
| 100 | ||
| 101 | private ResolvedLink resolve( final ResolvedLink link ) { | |
| 102 | var url = link.getUrl(); | |
| 103 | final var protocol = getProtocol( url ); | |
| 104 | ||
| 105 | try { | |
| 106 | if( protocol.isHttp() ) { | |
| 107 | return valid( link, url ); | |
| 108 | } | |
| 109 | } catch( final Exception ignored ) { | |
| 110 | // Try to resolve the image path, dynamically. | |
| 111 | } | |
| 112 | ||
| 113 | try { | |
| 114 | final Path imagePrefix = getImagePrefix().toPath(); | |
| 115 | ||
| 116 | // Path to the file being edited. | |
| 117 | Path editPath = getEditPath(); | |
| 118 | ||
| 119 | // If there is no parent path to the file, it means the file has not | |
| 120 | // been saved. Default to using the value from the user's preferences. | |
| 121 | // The user's preferences will be defaulted to a the application's | |
| 122 | // starting directory. | |
| 123 | editPath = editPath == null | |
| 124 | ? imagePrefix | |
| 125 | : Path.of( editPath.toString(), imagePrefix.toString() ); | |
| 126 | ||
| 127 | final var urlExt = getExtension( url ); | |
| 128 | url = removeExtension( url ); | |
| 129 | ||
| 130 | final var suffixes = urlExt + ' ' + getImageExtensions(); | |
| 131 | final var imagePathPrefix = Path.of( editPath.toString(), url ); | |
| 132 | var suffix = ".*"; | |
| 133 | boolean missing = true; | |
| 134 | ||
| 135 | // Iterate over the user's preferred image file type extensions. | |
| 136 | for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) { | |
| 137 | final String imagePath = format( "%s.%s", imagePathPrefix, ext ); | |
| 138 | final File file = new File( imagePath ); | |
| 139 | ||
| 140 | if( file.exists() ) { | |
| 141 | url = file.toString(); | |
| 142 | missing = false; | |
| 143 | break; | |
| 144 | } | |
| 145 | else if( !urlExt.isBlank() ) { | |
| 146 | // The file is missing because the user specified a prefix. | |
| 147 | suffix = urlExt; | |
| 148 | break; | |
| 149 | } | |
| 150 | } | |
| 151 | ||
| 152 | if( missing ) { | |
| 153 | throw new MissingFileException( imagePathPrefix + suffix ); | |
| 154 | } | |
| 155 | ||
| 156 | if( protocol.isFile() ) { | |
| 157 | url = "file://" + url; | |
| 158 | } | |
| 159 | ||
| 160 | return valid( link, url ); | |
| 161 | } catch( final Exception ex ) { | |
| 162 | alert( ex ); | |
| 163 | } | |
| 164 | ||
| 165 | return link; | |
| 166 | } | |
| 167 | ||
| 168 | private ResolvedLink valid( final ResolvedLink link, final String url ) { | |
| 169 | return link.withStatus( LinkStatus.VALID ).withUrl( url ); | |
| 170 | } | |
| 171 | ||
| 172 | private File getImagePrefix() { | |
| 173 | return mImagesUserPrefix; | |
| 174 | } | |
| 175 | ||
| 176 | private String getImageExtensions() { | |
| 177 | return mImageExtensions; | |
| 178 | } | |
| 179 | ||
| 180 | private Path getEditPath() { | |
| 181 | return mPath.getParent(); | |
| 182 | } | |
| 183 | } | |
| 184 | ||
| 185 | private final Path mPath; | |
| 186 | ||
| 187 | private ImageLinkExtension( @NotNull final Path path ) { | |
| 188 | mPath = path; | |
| 189 | } | |
| 190 | ||
| 191 | @Override | |
| 192 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 193 | } | |
| 194 | ||
| 195 | @Override | |
| 196 | public void extend( @NotNull final Builder builder, | |
| 197 | @NotNull final String rendererType ) { | |
| 198 | builder.linkResolverFactory( new Factory() ); | |
| 199 | } | |
| 200 | ||
| 201 | private UserPreferences getUserPreferences() { | |
| 202 | return UserPreferences.getInstance(); | |
| 203 | } | |
| 204 | } | |
| 1 | 205 |
| 1 | package com.keenwrite.processors.markdown; | |
| 2 | ||
| 3 | import com.vladsch.flexmark.ast.Text; | |
| 4 | import com.vladsch.flexmark.html.HtmlWriter; | |
| 5 | import com.vladsch.flexmark.html.renderer.NodeRenderer; | |
| 6 | import com.vladsch.flexmark.html.renderer.NodeRendererContext; | |
| 7 | import com.vladsch.flexmark.html.renderer.NodeRendererFactory; | |
| 8 | import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | |
| 9 | import com.vladsch.flexmark.util.ast.TextCollectingVisitor; | |
| 10 | import com.vladsch.flexmark.util.data.DataHolder; | |
| 11 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 12 | import org.jetbrains.annotations.NotNull; | |
| 13 | import org.jetbrains.annotations.Nullable; | |
| 14 | ||
| 15 | import java.util.LinkedHashMap; | |
| 16 | import java.util.Map; | |
| 17 | import java.util.Set; | |
| 18 | ||
| 19 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 20 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | |
| 21 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 22 | ||
| 23 | /** | |
| 24 | * Responsible for substituting multi-codepoint glyphs with single codepoint | |
| 25 | * glyphs. The text is adorned with ligatures prior to rendering as HTML. | |
| 26 | * This requires a font that supports ligatures. | |
| 27 | * <p> | |
| 28 | * TODO: #81 -- I18N | |
| 29 | * </p> | |
| 30 | */ | |
| 31 | public class LigatureExtension implements HtmlRendererExtension { | |
| 32 | /** | |
| 33 | * Retain insertion order so that ligature substitution uses longer ligatures | |
| 34 | * ahead of shorter ligatures. The word "ruffian" should use the "ffi" | |
| 35 | * ligature, not the "ff" ligature. | |
| 36 | */ | |
| 37 | private static final Map<String, String> LIGATURES = new LinkedHashMap<>(); | |
| 38 | ||
| 39 | static { | |
| 40 | LIGATURES.put( "ffi", "\uFB03" ); | |
| 41 | LIGATURES.put( "ffl", "\uFB04" ); | |
| 42 | LIGATURES.put( "ff", "\uFB00" ); | |
| 43 | LIGATURES.put( "fi", "\uFB01" ); | |
| 44 | LIGATURES.put( "fl", "\uFB02" ); | |
| 45 | LIGATURES.put( "ft", "\uFB05" ); | |
| 46 | LIGATURES.put( "AE", "\u00C6" ); | |
| 47 | LIGATURES.put( "OE", "\u0152" ); | |
| 48 | // "ae", "\u00E6", | |
| 49 | // "oe", "\u0153", | |
| 50 | } | |
| 51 | ||
| 52 | private static class LigatureRenderer implements NodeRenderer { | |
| 53 | private final TextCollectingVisitor mVisitor = new TextCollectingVisitor(); | |
| 54 | ||
| 55 | @SuppressWarnings("unused") | |
| 56 | public LigatureRenderer( final DataHolder options ) { | |
| 57 | } | |
| 58 | ||
| 59 | @Override | |
| 60 | public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | |
| 61 | return Set.of( new NodeRenderingHandler<>( | |
| 62 | Text.class, LigatureRenderer.this::render ) ); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * This will pick the fastest string replacement algorithm based on the | |
| 67 | * text length. The insertion order of the {@link #LIGATURES} is | |
| 68 | * important to give precedence to longer ligatures. | |
| 69 | * | |
| 70 | * @param textNode The text node containing text to replace with ligatures. | |
| 71 | * @param context Not used. | |
| 72 | * @param html Where to write the text adorned with ligatures. | |
| 73 | */ | |
| 74 | private void render( | |
| 75 | @NotNull final Text textNode, | |
| 76 | @NotNull final NodeRendererContext context, | |
| 77 | @NotNull final HtmlWriter html ) { | |
| 78 | final var text = mVisitor.collectAndGetText( textNode ); | |
| 79 | html.text( replace( text, LIGATURES ) ); | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | private static class Factory implements NodeRendererFactory { | |
| 84 | @NotNull | |
| 85 | @Override | |
| 86 | public NodeRenderer apply( @NotNull DataHolder options ) { | |
| 87 | return new LigatureRenderer( options ); | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | private LigatureExtension() { | |
| 92 | } | |
| 93 | ||
| 94 | @Override | |
| 95 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 96 | } | |
| 97 | ||
| 98 | @Override | |
| 99 | public void extend( @NotNull final Builder builder, | |
| 100 | @NotNull final String rendererType ) { | |
| 101 | if( "HTML".equalsIgnoreCase( rendererType ) ) { | |
| 102 | builder.nodeRendererFactory( new Factory() ); | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | public static LigatureExtension create() { | |
| 107 | return new LigatureExtension(); | |
| 108 | } | |
| 109 | } | |
| 1 | 110 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.markdown; | |
| 29 | ||
| 30 | import com.keenwrite.processors.AbstractProcessor; | |
| 31 | import com.keenwrite.processors.Processor; | |
| 32 | import com.vladsch.flexmark.ext.definition.DefinitionExtension; | |
| 33 | import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension; | |
| 34 | import com.vladsch.flexmark.ext.superscript.SuperscriptExtension; | |
| 35 | import com.vladsch.flexmark.ext.tables.TablesExtension; | |
| 36 | import com.vladsch.flexmark.ext.typographic.TypographicExtension; | |
| 37 | import com.vladsch.flexmark.html.HtmlRenderer; | |
| 38 | import com.vladsch.flexmark.parser.Parser; | |
| 39 | import com.vladsch.flexmark.util.ast.IParse; | |
| 40 | import com.vladsch.flexmark.util.ast.Node; | |
| 41 | import com.vladsch.flexmark.util.misc.Extension; | |
| 42 | ||
| 43 | import java.nio.file.Path; | |
| 44 | import java.util.ArrayList; | |
| 45 | import java.util.Collection; | |
| 46 | ||
| 47 | import static com.keenwrite.Constants.USER_DIRECTORY; | |
| 48 | ||
| 49 | /** | |
| 50 | * Responsible for parsing a Markdown document and rendering it as HTML. | |
| 51 | */ | |
| 52 | public class MarkdownProcessor extends AbstractProcessor<String> { | |
| 53 | ||
| 54 | private final HtmlRenderer mRenderer; | |
| 55 | private final IParse mParser; | |
| 56 | ||
| 57 | public MarkdownProcessor( | |
| 58 | final Processor<String> successor ) { | |
| 59 | this( successor, Path.of( USER_DIRECTORY ) ); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Constructs a new Markdown processor that can create HTML documents. | |
| 64 | * | |
| 65 | * @param successor Usually the HTML Preview Processor. | |
| 66 | */ | |
| 67 | public MarkdownProcessor( | |
| 68 | final Processor<String> successor, final Path path ) { | |
| 69 | super( successor ); | |
| 70 | ||
| 71 | // Standard extensions | |
| 72 | final Collection<Extension> extensions = new ArrayList<>(); | |
| 73 | extensions.add( DefinitionExtension.create() ); | |
| 74 | extensions.add( StrikethroughSubscriptExtension.create() ); | |
| 75 | extensions.add( SuperscriptExtension.create() ); | |
| 76 | extensions.add( TablesExtension.create() ); | |
| 77 | extensions.add( TypographicExtension.create() ); | |
| 78 | ||
| 79 | // Allows referencing image files via relative paths and dynamic file types. | |
| 80 | extensions.add( ImageLinkExtension.create( path ) ); | |
| 81 | extensions.add( BlockExtension.create() ); | |
| 82 | extensions.add( TeXExtension.create() ); | |
| 83 | ||
| 84 | // TODO: https://github.com/FAlthausen/Vollkorn-Typeface/issues/38 | |
| 85 | // TODO: Uncomment when Vollkorn ligatures are fixed. | |
| 86 | // extensions.add( LigatureExtension.create() ); | |
| 87 | ||
| 88 | mRenderer = HtmlRenderer.builder().extensions( extensions ).build(); | |
| 89 | mParser = Parser.builder() | |
| 90 | .extensions( extensions ) | |
| 91 | .build(); | |
| 92 | } | |
| 93 | ||
| 94 | /** | |
| 95 | * Converts the given Markdown string into HTML, without the doctype, html, | |
| 96 | * head, and body tags. | |
| 97 | * | |
| 98 | * @param markdown The string to convert from Markdown to HTML. | |
| 99 | * @return The HTML representation of the Markdown document. | |
| 100 | */ | |
| 101 | @Override | |
| 102 | public String apply( final String markdown ) { | |
| 103 | return toHtml( markdown ); | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Returns the AST in the form of a node for the given markdown document. This | |
| 108 | * can be used, for example, to determine if a hyperlink exists inside of a | |
| 109 | * paragraph. | |
| 110 | * | |
| 111 | * @param markdown The markdown to convert into an AST. | |
| 112 | * @return The markdown AST for the given text (usually a paragraph). | |
| 113 | */ | |
| 114 | public Node toNode( final String markdown ) { | |
| 115 | return parse( markdown ); | |
| 116 | } | |
| 117 | ||
| 118 | /** | |
| 119 | * Helper method to create an AST given some markdown. | |
| 120 | * | |
| 121 | * @param markdown The markdown to parse. | |
| 122 | * @return The root node of the markdown tree. | |
| 123 | */ | |
| 124 | private Node parse( final String markdown ) { | |
| 125 | return getParser().parse( markdown ); | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Converts a string of markdown into HTML. | |
| 130 | * | |
| 131 | * @param markdown The markdown text to convert to HTML, must not be null. | |
| 132 | * @return The markdown rendered as an HTML document. | |
| 133 | */ | |
| 134 | private String toHtml( final String markdown ) { | |
| 135 | return getRenderer().render( parse( markdown ) ); | |
| 136 | } | |
| 137 | ||
| 138 | /** | |
| 139 | * Creates the Markdown document processor. | |
| 140 | * | |
| 141 | * @return A Parser that can build an abstract syntax tree. | |
| 142 | */ | |
| 143 | private IParse getParser() { | |
| 144 | return mParser; | |
| 145 | } | |
| 146 | ||
| 147 | private HtmlRenderer getRenderer() { | |
| 148 | return mRenderer; | |
| 149 | } | |
| 150 | } | |
| 1 | 151 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.markdown; | |
| 29 | ||
| 30 | import com.keenwrite.processors.markdown.tex.TeXInlineDelimiterProcessor; | |
| 31 | import com.keenwrite.processors.markdown.tex.TeXNodeRenderer; | |
| 32 | import com.vladsch.flexmark.html.HtmlRenderer; | |
| 33 | import com.vladsch.flexmark.parser.Parser; | |
| 34 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 35 | import org.jetbrains.annotations.NotNull; | |
| 36 | ||
| 37 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 38 | import static com.vladsch.flexmark.parser.Parser.ParserExtension; | |
| 39 | ||
| 40 | /** | |
| 41 | * Responsible for wrapping delimited TeX code in Markdown into an XML element | |
| 42 | * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes | |
| 43 | * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer | |
| 44 | * is responsible for converting the TeX code for display. This avoids inserting | |
| 45 | * SVG code into the Markdown document, which the parser would then have to | |
| 46 | * iterate---a <em>very</em> wasteful operation that impacts front-end | |
| 47 | * performance. | |
| 48 | */ | |
| 49 | public class TeXExtension implements ParserExtension, HtmlRendererExtension { | |
| 50 | /** | |
| 51 | * Creates an extension capable of handling delimited TeX code in Markdown. | |
| 52 | * | |
| 53 | * @return The new {@link TeXExtension}, never {@code null}. | |
| 54 | */ | |
| 55 | public static TeXExtension create() { | |
| 56 | return new TeXExtension(); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Force using the {@link #create()} method for consistency. | |
| 61 | */ | |
| 62 | private TeXExtension() { | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Adds the TeX extension for HTML document export types. | |
| 67 | * | |
| 68 | * @param builder The document builder. | |
| 69 | * @param rendererType Indicates the document type to be built. | |
| 70 | */ | |
| 71 | @Override | |
| 72 | public void extend( @NotNull final HtmlRenderer.Builder builder, | |
| 73 | @NotNull final String rendererType ) { | |
| 74 | if( "HTML".equalsIgnoreCase( rendererType ) ) { | |
| 75 | builder.nodeRendererFactory( new TeXNodeRenderer.Factory() ); | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | @Override | |
| 80 | public void extend( final Parser.Builder builder ) { | |
| 81 | builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() ); | |
| 82 | } | |
| 83 | ||
| 84 | @Override | |
| 85 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 86 | } | |
| 87 | ||
| 88 | @Override | |
| 89 | public void parserOptions( final MutableDataHolder options ) { | |
| 90 | } | |
| 91 | } | |
| 1 | 92 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.markdown.tex; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.parser.InlineParser; | |
| 31 | import com.vladsch.flexmark.parser.core.delimiter.Delimiter; | |
| 32 | import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor; | |
| 33 | import com.vladsch.flexmark.parser.delimiter.DelimiterRun; | |
| 34 | import com.vladsch.flexmark.util.ast.Node; | |
| 35 | ||
| 36 | public class TeXInlineDelimiterProcessor implements DelimiterProcessor { | |
| 37 | ||
| 38 | @Override | |
| 39 | public void process( final Delimiter opener, final Delimiter closer, | |
| 40 | final int delimitersUsed ) { | |
| 41 | final var node = new TeXNode(); | |
| 42 | opener.moveNodesBetweenDelimitersTo(node, closer); | |
| 43 | } | |
| 44 | ||
| 45 | @Override | |
| 46 | public char getOpeningCharacter() { | |
| 47 | return '$'; | |
| 48 | } | |
| 49 | ||
| 50 | @Override | |
| 51 | public char getClosingCharacter() { | |
| 52 | return '$'; | |
| 53 | } | |
| 54 | ||
| 55 | @Override | |
| 56 | public int getMinLength() { | |
| 57 | return 1; | |
| 58 | } | |
| 59 | ||
| 60 | /** | |
| 61 | * Allow for $ or $$. | |
| 62 | * | |
| 63 | * @param opener One or more opening delimiter characters. | |
| 64 | * @param closer One or more closing delimiter characters. | |
| 65 | * @return The number of delimiters to use to determine whether a valid | |
| 66 | * opening delimiter expression is found. | |
| 67 | */ | |
| 68 | @Override | |
| 69 | public int getDelimiterUse( | |
| 70 | final DelimiterRun opener, final DelimiterRun closer ) { | |
| 71 | return 1; | |
| 72 | } | |
| 73 | ||
| 74 | @Override | |
| 75 | public boolean canBeOpener( final String before, | |
| 76 | final String after, | |
| 77 | final boolean leftFlanking, | |
| 78 | final boolean rightFlanking, | |
| 79 | final boolean beforeIsPunctuation, | |
| 80 | final boolean afterIsPunctuation, | |
| 81 | final boolean beforeIsWhitespace, | |
| 82 | final boolean afterIsWhiteSpace ) { | |
| 83 | return leftFlanking; | |
| 84 | } | |
| 85 | ||
| 86 | @Override | |
| 87 | public boolean canBeCloser( final String before, | |
| 88 | final String after, | |
| 89 | final boolean leftFlanking, | |
| 90 | final boolean rightFlanking, | |
| 91 | final boolean beforeIsPunctuation, | |
| 92 | final boolean afterIsPunctuation, | |
| 93 | final boolean beforeIsWhitespace, | |
| 94 | final boolean afterIsWhiteSpace ) { | |
| 95 | return rightFlanking; | |
| 96 | } | |
| 97 | ||
| 98 | @Override | |
| 99 | public Node unmatchedDelimiterNode( | |
| 100 | final InlineParser inlineParser, final DelimiterRun delimiter ) { | |
| 101 | return null; | |
| 102 | } | |
| 103 | ||
| 104 | @Override | |
| 105 | public boolean skipNonOpenerCloser() { | |
| 106 | return false; | |
| 107 | } | |
| 108 | } | |
| 1 | 109 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.markdown.tex; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.ast.DelimitedNodeImpl; | |
| 31 | ||
| 32 | public class TeXNode extends DelimitedNodeImpl { | |
| 33 | /** | |
| 34 | * TeX expression wrapped in a {@code <tex>} element. | |
| 35 | */ | |
| 36 | public static final String HTML_TEX = "tex"; | |
| 37 | ||
| 38 | public TeXNode() { | |
| 39 | } | |
| 40 | } | |
| 1 | 41 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.markdown.tex; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.html.HtmlWriter; | |
| 31 | import com.vladsch.flexmark.html.renderer.NodeRenderer; | |
| 32 | import com.vladsch.flexmark.html.renderer.NodeRendererContext; | |
| 33 | import com.vladsch.flexmark.html.renderer.NodeRendererFactory; | |
| 34 | import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | |
| 35 | import com.vladsch.flexmark.util.data.DataHolder; | |
| 36 | import org.jetbrains.annotations.NotNull; | |
| 37 | import org.jetbrains.annotations.Nullable; | |
| 38 | ||
| 39 | import java.util.Set; | |
| 40 | ||
| 41 | import static com.keenwrite.processors.markdown.tex.TeXNode.HTML_TEX; | |
| 42 | ||
| 43 | public class TeXNodeRenderer implements NodeRenderer { | |
| 44 | ||
| 45 | public static class Factory implements NodeRendererFactory { | |
| 46 | @NotNull | |
| 47 | @Override | |
| 48 | public NodeRenderer apply( @NotNull DataHolder options ) { | |
| 49 | return new TeXNodeRenderer(); | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | @Override | |
| 54 | public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | |
| 55 | return Set.of( new NodeRenderingHandler<>( TeXNode.class, this::render ) ); | |
| 56 | } | |
| 57 | ||
| 58 | private void render( final TeXNode node, | |
| 59 | final NodeRendererContext context, | |
| 60 | final HtmlWriter html ) { | |
| 61 | html.tag( HTML_TEX ); | |
| 62 | html.raw( node.getText() ); | |
| 63 | html.closeTag( HTML_TEX ); | |
| 64 | } | |
| 65 | } | |
| 1 | 66 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for common behaviour across all text replacer implementations. | |
| 34 | */ | |
| 35 | public abstract class AbstractTextReplacer implements TextReplacer { | |
| 36 | ||
| 37 | /** | |
| 38 | * Default (empty) constructor. | |
| 39 | */ | |
| 40 | protected AbstractTextReplacer() { | |
| 41 | } | |
| 42 | ||
| 43 | protected String[] keys( final Map<String, String> map ) { | |
| 44 | return map.keySet().toArray( new String[ 0 ] ); | |
| 45 | } | |
| 46 | ||
| 47 | protected String[] values( final Map<String, String> map ) { | |
| 48 | return map.values().toArray( new String[ 0 ] ); | |
| 49 | } | |
| 50 | } | |
| 1 | 51 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | import org.ahocorasick.trie.Emit; | |
| 32 | import org.ahocorasick.trie.Trie.TrieBuilder; | |
| 33 | import static org.ahocorasick.trie.Trie.builder; | |
| 34 | ||
| 35 | /** | |
| 36 | * Replaces text using an Aho-Corasick algorithm. | |
| 37 | */ | |
| 38 | public class AhoCorasickReplacer extends AbstractTextReplacer { | |
| 39 | ||
| 40 | /** | |
| 41 | * Default (empty) constructor. | |
| 42 | */ | |
| 43 | protected AhoCorasickReplacer() { | |
| 44 | } | |
| 45 | ||
| 46 | @Override | |
| 47 | public String replace( final String text, final Map<String, String> map ) { | |
| 48 | // Create a buffer sufficiently large that re-allocations are minimized. | |
| 49 | final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) ); | |
| 50 | ||
| 51 | // The TrieBuilder should only match whole words and ignore overlaps (there | |
| 52 | // shouldn't be any). | |
| 53 | final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps(); | |
| 54 | ||
| 55 | for( final String key : keys( map ) ) { | |
| 56 | builder.addKeyword( key ); | |
| 57 | } | |
| 58 | ||
| 59 | int index = 0; | |
| 60 | ||
| 61 | // Replace all instances with dereferenced variables. | |
| 62 | for( final Emit emit : builder.build().parseText( text ) ) { | |
| 63 | sb.append( text, index, emit.getStart() ); | |
| 64 | sb.append( map.get( emit.getKeyword() ) ); | |
| 65 | index = emit.getEnd() + 1; | |
| 66 | } | |
| 67 | ||
| 68 | // Add the remainder of the string (contains no more matches). | |
| 69 | sb.append( text.substring( index ) ); | |
| 70 | ||
| 71 | return sb.toString(); | |
| 72 | } | |
| 73 | } | |
| 1 | 74 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | import static org.apache.commons.lang3.StringUtils.replaceEach; | |
| 33 | ||
| 34 | /** | |
| 35 | * Replaces text using Apache's StringUtils.replaceEach method. | |
| 36 | */ | |
| 37 | public class StringUtilsReplacer extends AbstractTextReplacer { | |
| 38 | ||
| 39 | /** | |
| 40 | * Default (empty) constructor. | |
| 41 | */ | |
| 42 | protected StringUtilsReplacer() { | |
| 43 | } | |
| 44 | ||
| 45 | @Override | |
| 46 | public String replace( final String text, final Map<String, String> map ) { | |
| 47 | return replaceEach( text, keys( map ), values( map ) ); | |
| 48 | } | |
| 49 | } | |
| 1 | 50 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | /** | |
| 33 | * Used to generate a class capable of efficiently replacing variable | |
| 34 | * definitions with their values. | |
| 35 | */ | |
| 36 | public final class TextReplacementFactory { | |
| 37 | ||
| 38 | private static final TextReplacer APACHE = new StringUtilsReplacer(); | |
| 39 | private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer(); | |
| 40 | ||
| 41 | /** | |
| 42 | * Returns a text search/replacement instance that is reasonably optimal for | |
| 43 | * the given length of text. | |
| 44 | * | |
| 45 | * @param length The length of text that requires some search and replacing. | |
| 46 | * @return A class that can search and replace text with utmost expediency. | |
| 47 | */ | |
| 48 | public static TextReplacer getTextReplacer( final int length ) { | |
| 49 | // After about 1,500 characters, the StringUtils implementation is less | |
| 50 | // performant than the Aho-Corsick implementation. | |
| 51 | // | |
| 52 | // See http://stackoverflow.com/a/40836618/59087 | |
| 53 | return length < 1500 ? APACHE : AHO_CORASICK; | |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * Convenience method to instantiate a suitable text replacer algorithm and | |
| 58 | * perform a replacement using the given map. At this point, the values should | |
| 59 | * be already dereferenced and ready to be substituted verbatim; any | |
| 60 | * recursively defined values must have been interpolated previously. | |
| 61 | * | |
| 62 | * @param text The text containing zero or more variables to replace. | |
| 63 | * @param map The map of variables to their dereferenced values. | |
| 64 | * @return The text with all variables replaced. | |
| 65 | */ | |
| 66 | public static String replace( | |
| 67 | final String text, final Map<String, String> map ) { | |
| 68 | return getTextReplacer( text.length() ).replace( text, map ); | |
| 69 | } | |
| 70 | } | |
| 1 | 71 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | /** | |
| 33 | * Defines the ability to replace text given a set of keys and values. | |
| 34 | */ | |
| 35 | public interface TextReplacer { | |
| 36 | ||
| 37 | /** | |
| 38 | * Searches through the given text for any of the keys given in the map and | |
| 39 | * replaces the keys that appear in the text with the key's corresponding | |
| 40 | * value. | |
| 41 | * | |
| 42 | * @param text The text that contains zero or more keys. | |
| 43 | * @param map The set of keys mapped to replacement values. | |
| 44 | * @return The given text with all keys replaced with corresponding values. | |
| 45 | */ | |
| 46 | String replace( String text, Map<String, String> map ); | |
| 47 | } | |
| 1 | 48 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service; | |
| 29 | ||
| 30 | import com.dlsc.preferencesfx.PreferencesFx; | |
| 31 | ||
| 32 | import java.util.prefs.BackingStoreException; | |
| 33 | import java.util.prefs.Preferences; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for persisting options that are safe to load before the UI | |
| 37 | * is shown. This can include items like window dimensions, last file | |
| 38 | * opened, split pane locations, and more. This cannot be used to persist | |
| 39 | * options that are user-controlled (i.e., all options available through | |
| 40 | * {@link PreferencesFx}). | |
| 41 | */ | |
| 42 | public interface Options extends Service { | |
| 43 | ||
| 44 | /** | |
| 45 | * Returns the {@link Preferences} that persist settings that cannot | |
| 46 | * be configured via the user interface. | |
| 47 | * | |
| 48 | * @return A valid {@link Preferences} instance, never {@code null}. | |
| 49 | */ | |
| 50 | Preferences getState(); | |
| 51 | ||
| 52 | /** | |
| 53 | * Stores the key and value into the user preferences to be loaded the next | |
| 54 | * time the application is launched. | |
| 55 | * | |
| 56 | * @param key Name of the key to persist along with its value. | |
| 57 | * @param value Value to associate with the key. | |
| 58 | * @throws BackingStoreException Could not persist the change. | |
| 59 | */ | |
| 60 | void put( String key, String value ) throws BackingStoreException; | |
| 61 | ||
| 62 | /** | |
| 63 | * Retrieves the value for a key in the user preferences. | |
| 64 | * | |
| 65 | * @param key Retrieve the value of this key. | |
| 66 | * @param defaultValue The value to return in the event that the given key has | |
| 67 | * no associated value. | |
| 68 | * @return The value associated with the key. | |
| 69 | */ | |
| 70 | String get( String key, String defaultValue ); | |
| 71 | ||
| 72 | /** | |
| 73 | * Retrieves the value for a key in the user preferences. This will return | |
| 74 | * the empty string if the value cannot be found. | |
| 75 | * | |
| 76 | * @param key The key to find in the preferences. | |
| 77 | * @return A non-null, possibly empty value for the key. | |
| 78 | */ | |
| 79 | String get( String key ); | |
| 80 | } | |
| 1 | 81 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service; | |
| 29 | ||
| 30 | /** | |
| 31 | * All services inherit from this one. | |
| 32 | */ | |
| 33 | public interface Service { | |
| 34 | } | |
| 1 | 35 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service; | |
| 29 | ||
| 30 | import java.util.Iterator; | |
| 31 | import java.util.List; | |
| 32 | ||
| 33 | /** | |
| 34 | * Defines how settings and options can be retrieved. | |
| 35 | */ | |
| 36 | public interface Settings extends Service { | |
| 37 | ||
| 38 | /** | |
| 39 | * Returns a setting property or its default value. | |
| 40 | * | |
| 41 | * @param property The property key name to obtain its value. | |
| 42 | * @param defaultValue The default value to return iff the property cannot be | |
| 43 | * found. | |
| 44 | * @return The property value for the given property key. | |
| 45 | */ | |
| 46 | String getSetting( String property, String defaultValue ); | |
| 47 | ||
| 48 | /** | |
| 49 | * Returns a setting property or its default value. | |
| 50 | * | |
| 51 | * @param property The property key name to obtain its value. | |
| 52 | * @param defaultValue The default value to return iff the property cannot be | |
| 53 | * found. | |
| 54 | * @return The property value for the given property key. | |
| 55 | */ | |
| 56 | int getSetting( String property, int defaultValue ); | |
| 57 | ||
| 58 | /** | |
| 59 | * Returns a list of property names that begin with the given prefix. The | |
| 60 | * prefix is included in any matching results. This will return keys that | |
| 61 | * either match the prefix or start with the prefix followed by a dot ('.'). | |
| 62 | * For example, a prefix value of <code>the.property.name</code> will likely | |
| 63 | * return the expected results, but <code>the.property.name.</code> (note the | |
| 64 | * extraneous period) will probably not. | |
| 65 | * | |
| 66 | * @param prefix The prefix to compare against each property name. | |
| 67 | * @return The list of property names that have the given prefix. | |
| 68 | */ | |
| 69 | Iterator<String> getKeys( final String prefix ); | |
| 70 | ||
| 71 | /** | |
| 72 | * Convert the generic list of property objects into strings. | |
| 73 | * | |
| 74 | * @param property The property value to coerce. | |
| 75 | * @param defaults The defaults values to use should the property be unset. | |
| 76 | * @return The list of properties coerced from objects to strings. | |
| 77 | */ | |
| 78 | List<String> getStringSettingList( String property, List<String> defaults ); | |
| 79 | ||
| 80 | /** | |
| 81 | * Converts the generic list of property objects into strings. | |
| 82 | * | |
| 83 | * @param property The property value to coerce. | |
| 84 | * @return The list of properties coerced from objects to strings. | |
| 85 | */ | |
| 86 | List<String> getStringSettingList( String property ); | |
| 87 | } | |
| 1 | 88 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service; | |
| 29 | ||
| 30 | import java.io.IOException; | |
| 31 | import java.nio.file.Path; | |
| 32 | import java.util.Observer; | |
| 33 | ||
| 34 | /** | |
| 35 | * Listens for changes to file system files and directories. | |
| 36 | */ | |
| 37 | public interface Snitch extends Service, Runnable { | |
| 38 | ||
| 39 | /** | |
| 40 | * Adds an observer to the set of observers for this object, provided that it | |
| 41 | * is not the same as some observer already in the set. The order in which | |
| 42 | * notifications will be delivered to multiple observers is not specified. | |
| 43 | * | |
| 44 | * @param o The object to receive changed events for when monitored files | |
| 45 | * are changed. | |
| 46 | */ | |
| 47 | void addObserver( Observer o ); | |
| 48 | ||
| 49 | /** | |
| 50 | * Listens for changes to the path. If the path specifies a file, then only | |
| 51 | * notifications pertaining to that file are sent. Otherwise, change events | |
| 52 | * for the directory that contains the file are sent. This method must allow | |
| 53 | * for multiple calls to the same file without incurring additional listeners | |
| 54 | * or events. | |
| 55 | * | |
| 56 | * @param file Send notifications when this file changes, can be null. | |
| 57 | * @throws IOException Couldn't create a watcher for the given file. | |
| 58 | */ | |
| 59 | void listen( Path file ) throws IOException; | |
| 60 | ||
| 61 | /** | |
| 62 | * Removes the given file from the notifications list. | |
| 63 | * | |
| 64 | * @param file The file to stop monitoring for any changes, can be null. | |
| 65 | */ | |
| 66 | void ignore( Path file ); | |
| 67 | ||
| 68 | /** | |
| 69 | * Stop listening for events. | |
| 70 | */ | |
| 71 | void stop(); | |
| 72 | } | |
| 1 | 73 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service.events; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents a message that contains a title and content. | |
| 32 | */ | |
| 33 | public interface Notification { | |
| 34 | ||
| 35 | /** | |
| 36 | * Alert title. | |
| 37 | * | |
| 38 | * @return A non-null string to use as alert message title. | |
| 39 | */ | |
| 40 | String getTitle(); | |
| 41 | ||
| 42 | /** | |
| 43 | * Alert message content. | |
| 44 | * | |
| 45 | * @return A non-null string that contains information for the user. | |
| 46 | */ | |
| 47 | String getContent(); | |
| 48 | } | |
| 1 | 49 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service.events; | |
| 29 | ||
| 30 | import javafx.scene.control.Alert; | |
| 31 | import javafx.scene.control.ButtonType; | |
| 32 | import javafx.stage.Window; | |
| 33 | ||
| 34 | /** | |
| 35 | * Provides the application with a uniform way to notify the user of events. | |
| 36 | */ | |
| 37 | public interface Notifier { | |
| 38 | ||
| 39 | ButtonType YES = ButtonType.YES; | |
| 40 | ButtonType NO = ButtonType.NO; | |
| 41 | ButtonType CANCEL = ButtonType.CANCEL; | |
| 42 | ||
| 43 | /** | |
| 44 | * Constructs a default alert message text for a modal alert dialog. | |
| 45 | * | |
| 46 | * @param title The dialog box message title. | |
| 47 | * @param message The dialog box message content (needs formatting). | |
| 48 | * @param args The arguments to the message content that must be formatted. | |
| 49 | * @return The message suitable for building a modal alert dialog. | |
| 50 | */ | |
| 51 | Notification createNotification( | |
| 52 | String title, | |
| 53 | String message, | |
| 54 | Object... args ); | |
| 55 | ||
| 56 | /** | |
| 57 | * Creates an alert of alert type error with a message showing the cause of | |
| 58 | * the error. | |
| 59 | * | |
| 60 | * @param parent Dialog box owner (for modal purposes). | |
| 61 | * @param message The error message, title, and possibly more details. | |
| 62 | * @return A modal alert dialog box ready to display using showAndWait. | |
| 63 | */ | |
| 64 | Alert createError( Window parent, Notification message ); | |
| 65 | ||
| 66 | /** | |
| 67 | * Creates an alert of alert type confirmation with Yes/No/Cancel buttons. | |
| 68 | * | |
| 69 | * @param parent Dialog box owner (for modal purposes). | |
| 70 | * @param message The message, title, and possibly more details. | |
| 71 | * @return A modal alert dialog box ready to display using showAndWait. | |
| 72 | */ | |
| 73 | Alert createConfirmation( Window parent, Notification message ); | |
| 74 | } | |
| 1 | 75 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service.events.impl; | |
| 29 | ||
| 30 | import javafx.scene.Node; | |
| 31 | import javafx.scene.control.ButtonBar; | |
| 32 | import javafx.scene.control.DialogPane; | |
| 33 | ||
| 34 | import static com.keenwrite.Constants.SETTINGS; | |
| 35 | import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS; | |
| 36 | ||
| 37 | /** | |
| 38 | * Ensures a consistent button order for alert dialogs across platforms (because | |
| 39 | * the default button order on Linux defies all logic). | |
| 40 | */ | |
| 41 | public class ButtonOrderPane extends DialogPane { | |
| 42 | ||
| 43 | @Override | |
| 44 | protected Node createButtonBar() { | |
| 45 | final var node = (ButtonBar) super.createButtonBar(); | |
| 46 | node.setButtonOrder( getButtonOrder() ); | |
| 47 | return node; | |
| 48 | } | |
| 49 | ||
| 50 | private String getButtonOrder() { | |
| 51 | return getSetting( "dialog.alert.button.order.windows", | |
| 52 | BUTTON_ORDER_WINDOWS ); | |
| 53 | } | |
| 54 | ||
| 55 | @SuppressWarnings("SameParameterValue") | |
| 56 | private String getSetting( final String key, final String defaultValue ) { | |
| 57 | return SETTINGS.getSetting( key, defaultValue ); | |
| 58 | } | |
| 59 | } | |
| 1 | 60 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service.events.impl; | |
| 29 | ||
| 30 | import com.keenwrite.service.events.Notification; | |
| 31 | ||
| 32 | import java.text.MessageFormat; | |
| 33 | ||
| 34 | /** | |
| 35 | * Responsible for alerting the user to prominent information. | |
| 36 | */ | |
| 37 | public class DefaultNotification implements Notification { | |
| 38 | ||
| 39 | private final String title; | |
| 40 | private final String content; | |
| 41 | ||
| 42 | /** | |
| 43 | * Constructs default message text for a notification. | |
| 44 | * | |
| 45 | * @param title The message title. | |
| 46 | * @param message The message content (needs formatting). | |
| 47 | * @param args The arguments to the message content that must be formatted. | |
| 48 | */ | |
| 49 | public DefaultNotification( | |
| 50 | final String title, | |
| 51 | final String message, | |
| 52 | final Object... args ) { | |
| 53 | this.title = title; | |
| 54 | this.content = MessageFormat.format( message, args ); | |
| 55 | } | |
| 56 | ||
| 57 | @Override | |
| 58 | public String getTitle() { | |
| 59 | return this.title; | |
| 60 | } | |
| 61 | ||
| 62 | @Override | |
| 63 | public String getContent() { | |
| 64 | return this.content; | |
| 65 | } | |
| 66 | } | |
| 1 | 67 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service.events.impl; | |
| 29 | ||
| 30 | import com.keenwrite.service.events.Notification; | |
| 31 | import com.keenwrite.service.events.Notifier; | |
| 32 | import javafx.scene.control.Alert; | |
| 33 | import javafx.scene.control.Alert.AlertType; | |
| 34 | import javafx.stage.Window; | |
| 35 | ||
| 36 | import static javafx.scene.control.Alert.AlertType.CONFIRMATION; | |
| 37 | import static javafx.scene.control.Alert.AlertType.ERROR; | |
| 38 | ||
| 39 | /** | |
| 40 | * Provides the ability to notify the user of events that need attention, | |
| 41 | * such as prompting the user to confirm closing when there are unsaved changes. | |
| 42 | */ | |
| 43 | public final class DefaultNotifier implements Notifier { | |
| 44 | ||
| 45 | /** | |
| 46 | * Contains all the information that the user needs to know about a problem. | |
| 47 | * | |
| 48 | * @param title The context for the message. | |
| 49 | * @param message The message content (formatted with the given args). | |
| 50 | * @param args Parameters for the message content. | |
| 51 | * @return A notification instance, never null. | |
| 52 | */ | |
| 53 | @Override | |
| 54 | public Notification createNotification( | |
| 55 | final String title, | |
| 56 | final String message, | |
| 57 | final Object... args ) { | |
| 58 | return new DefaultNotification( title, message, args ); | |
| 59 | } | |
| 60 | ||
| 61 | private Alert createAlertDialog( | |
| 62 | final Window parent, | |
| 63 | final AlertType alertType, | |
| 64 | final Notification message ) { | |
| 65 | ||
| 66 | final Alert alert = new Alert( alertType ); | |
| 67 | ||
| 68 | alert.setDialogPane( new ButtonOrderPane() ); | |
| 69 | alert.setTitle( message.getTitle() ); | |
| 70 | alert.setHeaderText( null ); | |
| 71 | alert.setContentText( message.getContent() ); | |
| 72 | alert.initOwner( parent ); | |
| 73 | ||
| 74 | return alert; | |
| 75 | } | |
| 76 | ||
| 77 | @Override | |
| 78 | public Alert createConfirmation( final Window parent, | |
| 79 | final Notification message ) { | |
| 80 | final Alert alert = createAlertDialog( parent, CONFIRMATION, message ); | |
| 81 | ||
| 82 | alert.getButtonTypes().setAll( YES, NO, CANCEL ); | |
| 83 | ||
| 84 | return alert; | |
| 85 | } | |
| 86 | ||
| 87 | @Override | |
| 88 | public Alert createError( final Window parent, final Notification message ) { | |
| 89 | return createAlertDialog( parent, ERROR, message ); | |
| 90 | } | |
| 91 | } | |
| 1 | 92 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.service.impl; | |
| 28 | ||
| 29 | import com.keenwrite.service.Options; | |
| 30 | ||
| 31 | import java.util.prefs.BackingStoreException; | |
| 32 | import java.util.prefs.Preferences; | |
| 33 | ||
| 34 | import static com.keenwrite.Constants.PREFS_ROOT; | |
| 35 | import static com.keenwrite.Constants.PREFS_STATE; | |
| 36 | import static java.util.prefs.Preferences.userRoot; | |
| 37 | ||
| 38 | /** | |
| 39 | * Persistent options user can change at runtime. | |
| 40 | */ | |
| 41 | public class DefaultOptions implements Options { | |
| 42 | public DefaultOptions() { | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * This will throw IllegalArgumentException if the value exceeds the maximum | |
| 47 | * preferences value length. | |
| 48 | * | |
| 49 | * @param key The name of the key to associate with the value. | |
| 50 | * @param value The value to persist. | |
| 51 | * @throws BackingStoreException New value not persisted. | |
| 52 | */ | |
| 53 | @Override | |
| 54 | public void put( final String key, final String value ) | |
| 55 | throws BackingStoreException { | |
| 56 | getState().put( key, value ); | |
| 57 | getState().flush(); | |
| 58 | } | |
| 59 | ||
| 60 | @Override | |
| 61 | public String get( final String key, final String value ) { | |
| 62 | return getState().get( key, value ); | |
| 63 | } | |
| 64 | ||
| 65 | @Override | |
| 66 | public String get( final String key ) { | |
| 67 | return get( key, "" ); | |
| 68 | } | |
| 69 | ||
| 70 | private Preferences getRootPreferences() { | |
| 71 | return userRoot().node( PREFS_ROOT ); | |
| 72 | } | |
| 73 | ||
| 74 | @Override | |
| 75 | public Preferences getState() { | |
| 76 | return getRootPreferences().node( PREFS_STATE ); | |
| 77 | } | |
| 78 | } | |
| 1 | 79 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service.impl; | |
| 29 | ||
| 30 | import com.keenwrite.service.Settings; | |
| 31 | import org.apache.commons.configuration2.PropertiesConfiguration; | |
| 32 | import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler; | |
| 33 | import org.apache.commons.configuration2.convert.ListDelimiterHandler; | |
| 34 | ||
| 35 | import java.io.InputStreamReader; | |
| 36 | import java.net.URL; | |
| 37 | import java.nio.charset.Charset; | |
| 38 | import java.util.Iterator; | |
| 39 | import java.util.List; | |
| 40 | ||
| 41 | import static com.keenwrite.Constants.PATH_PROPERTIES_SETTINGS; | |
| 42 | ||
| 43 | /** | |
| 44 | * Responsible for loading settings that help avoid hard-coded assumptions. | |
| 45 | */ | |
| 46 | public class DefaultSettings implements Settings { | |
| 47 | ||
| 48 | private static final char VALUE_SEPARATOR = ','; | |
| 49 | ||
| 50 | private PropertiesConfiguration mProperties; | |
| 51 | ||
| 52 | public DefaultSettings() { | |
| 53 | setProperties( createProperties() ); | |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * Returns the value of a string property. | |
| 58 | * | |
| 59 | * @param property The property key. | |
| 60 | * @param defaultValue The value to return if no property key has been set. | |
| 61 | * @return The property key value, or defaultValue when no key found. | |
| 62 | */ | |
| 63 | @Override | |
| 64 | public String getSetting( final String property, final String defaultValue ) { | |
| 65 | return getSettings().getString( property, defaultValue ); | |
| 66 | } | |
| 67 | ||
| 68 | /** | |
| 69 | * Returns the value of a string property. | |
| 70 | * | |
| 71 | * @param property The property key. | |
| 72 | * @param defaultValue The value to return if no property key has been set. | |
| 73 | * @return The property key value, or defaultValue when no key found. | |
| 74 | */ | |
| 75 | @Override | |
| 76 | public int getSetting( final String property, final int defaultValue ) { | |
| 77 | return getSettings().getInt( property, defaultValue ); | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * Convert the generic list of property objects into strings. | |
| 82 | * | |
| 83 | * @param property The property value to coerce. | |
| 84 | * @param defaults The defaults values to use should the property be unset. | |
| 85 | * @return The list of properties coerced from objects to strings. | |
| 86 | */ | |
| 87 | @Override | |
| 88 | public List<String> getStringSettingList( | |
| 89 | final String property, final List<String> defaults ) { | |
| 90 | return getSettings().getList( String.class, property, defaults ); | |
| 91 | } | |
| 92 | ||
| 93 | /** | |
| 94 | * Convert a list of property objects into strings, with no default value. | |
| 95 | * | |
| 96 | * @param property The property value to coerce. | |
| 97 | * @return The list of properties coerced from objects to strings. | |
| 98 | */ | |
| 99 | @Override | |
| 100 | public List<String> getStringSettingList( final String property ) { | |
| 101 | return getStringSettingList( property, null ); | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Returns a list of property names that begin with the given prefix. | |
| 106 | * | |
| 107 | * @param prefix The prefix to compare against each property name. | |
| 108 | * @return The list of property names that have the given prefix. | |
| 109 | */ | |
| 110 | @Override | |
| 111 | public Iterator<String> getKeys( final String prefix ) { | |
| 112 | return getSettings().getKeys( prefix ); | |
| 113 | } | |
| 114 | ||
| 115 | private PropertiesConfiguration createProperties() { | |
| 116 | final var url = getPropertySource(); | |
| 117 | final var configuration = new PropertiesConfiguration(); | |
| 118 | ||
| 119 | if( url != null ) { | |
| 120 | try( final var reader = new InputStreamReader( | |
| 121 | url.openStream(), getDefaultEncoding() ) ) { | |
| 122 | configuration.setListDelimiterHandler( createListDelimiterHandler() ); | |
| 123 | configuration.read( reader ); | |
| 124 | } catch( final Exception ex ) { | |
| 125 | throw new RuntimeException( ex ); | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | return configuration; | |
| 130 | } | |
| 131 | ||
| 132 | protected Charset getDefaultEncoding() { | |
| 133 | return Charset.defaultCharset(); | |
| 134 | } | |
| 135 | ||
| 136 | protected ListDelimiterHandler createListDelimiterHandler() { | |
| 137 | return new DefaultListDelimiterHandler( VALUE_SEPARATOR ); | |
| 138 | } | |
| 139 | ||
| 140 | private URL getPropertySource() { | |
| 141 | return DefaultSettings.class.getResource( PATH_PROPERTIES_SETTINGS ); | |
| 142 | } | |
| 143 | ||
| 144 | private void setProperties( final PropertiesConfiguration properties ) { | |
| 145 | mProperties = properties; | |
| 146 | } | |
| 147 | ||
| 148 | private PropertiesConfiguration getSettings() { | |
| 149 | return mProperties; | |
| 150 | } | |
| 151 | } | |
| 1 | 152 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.service.impl; | |
| 29 | ||
| 30 | import com.keenwrite.service.Snitch; | |
| 31 | ||
| 32 | import java.io.IOException; | |
| 33 | import java.nio.file.*; | |
| 34 | import java.util.Collections; | |
| 35 | import java.util.Map; | |
| 36 | import java.util.Observable; | |
| 37 | import java.util.Set; | |
| 38 | import java.util.concurrent.ConcurrentHashMap; | |
| 39 | ||
| 40 | import static com.keenwrite.Constants.APP_WATCHDOG_TIMEOUT; | |
| 41 | import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; | |
| 42 | ||
| 43 | /** | |
| 44 | * Listens for file changes. Other classes can register paths to be monitored | |
| 45 | * and listen for changes to those paths. | |
| 46 | */ | |
| 47 | public class DefaultSnitch extends Observable implements Snitch { | |
| 48 | ||
| 49 | /** | |
| 50 | * Service for listening to directories for modifications. | |
| 51 | */ | |
| 52 | private WatchService watchService; | |
| 53 | ||
| 54 | /** | |
| 55 | * Directories being monitored for changes. | |
| 56 | */ | |
| 57 | private Map<WatchKey, Path> keys; | |
| 58 | ||
| 59 | /** | |
| 60 | * Files that will kick off notification events if modified. | |
| 61 | */ | |
| 62 | private Set<Path> eavesdropped; | |
| 63 | ||
| 64 | /** | |
| 65 | * Set to true when running; set to false to stop listening. | |
| 66 | */ | |
| 67 | private volatile boolean listening; | |
| 68 | ||
| 69 | public DefaultSnitch() { | |
| 70 | } | |
| 71 | ||
| 72 | @Override | |
| 73 | public void stop() { | |
| 74 | setListening( false ); | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Adds a listener to the list of files to watch for changes. If the file is | |
| 79 | * already in the monitored list, this will return immediately. | |
| 80 | * | |
| 81 | * @param file Path to a file to watch for changes. | |
| 82 | * @throws IOException The file could not be monitored. | |
| 83 | */ | |
| 84 | @Override | |
| 85 | public void listen( final Path file ) throws IOException { | |
| 86 | if( file != null && getEavesdropped().add( file ) ) { | |
| 87 | final Path dir = toDirectory( file ); | |
| 88 | final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY ); | |
| 89 | ||
| 90 | getWatchMap().put( key, dir ); | |
| 91 | } | |
| 92 | } | |
| 93 | ||
| 94 | /** | |
| 95 | * Returns the given path to a file (or directory) as a directory. If the | |
| 96 | * given path is already a directory, it is returned. Otherwise, this returns | |
| 97 | * the directory that contains the file. This will fail if the file is stored | |
| 98 | * in the root folder. | |
| 99 | * | |
| 100 | * @param path The file to return as a directory, which should always be the | |
| 101 | * case. | |
| 102 | * @return The given path as a directory, if a file, otherwise the path | |
| 103 | * itself. | |
| 104 | */ | |
| 105 | private Path toDirectory( final Path path ) { | |
| 106 | return Files.isDirectory( path ) | |
| 107 | ? path | |
| 108 | : path.toFile().getParentFile().toPath(); | |
| 109 | } | |
| 110 | ||
| 111 | /** | |
| 112 | * Stop listening to the given file for change events. This fails silently. | |
| 113 | * | |
| 114 | * @param file The file to no longer monitor for changes. | |
| 115 | */ | |
| 116 | @Override | |
| 117 | public void ignore( final Path file ) { | |
| 118 | if( file != null ) { | |
| 119 | final Path directory = toDirectory( file ); | |
| 120 | ||
| 121 | // Remove all occurrences (there should be only one). | |
| 122 | getWatchMap().values().removeAll( Collections.singleton( directory ) ); | |
| 123 | ||
| 124 | // Remove all occurrences (there can be only one). | |
| 125 | getEavesdropped().remove( file ); | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | /** | |
| 130 | * Loops until stop is called, or the application is terminated. | |
| 131 | */ | |
| 132 | @Override | |
| 133 | @SuppressWarnings("BusyWait") | |
| 134 | public void run() { | |
| 135 | setListening( true ); | |
| 136 | ||
| 137 | while( isListening() ) { | |
| 138 | try { | |
| 139 | final WatchKey key = getWatchService().take(); | |
| 140 | final Path path = get( key ); | |
| 141 | ||
| 142 | // Prevent receiving two separate ENTRY_MODIFY events: file modified | |
| 143 | // and timestamp updated. Instead, receive one ENTRY_MODIFY event | |
| 144 | // with two counts. | |
| 145 | Thread.sleep( APP_WATCHDOG_TIMEOUT ); | |
| 146 | ||
| 147 | for( final WatchEvent<?> event : key.pollEvents() ) { | |
| 148 | final Path changed = path.resolve( (Path) event.context() ); | |
| 149 | ||
| 150 | if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) { | |
| 151 | setChanged(); | |
| 152 | notifyObservers( changed ); | |
| 153 | } | |
| 154 | } | |
| 155 | ||
| 156 | if( !key.reset() ) { | |
| 157 | ignore( path ); | |
| 158 | } | |
| 159 | } catch( final IOException | InterruptedException ex ) { | |
| 160 | // Stop eavesdropping. | |
| 161 | setListening( false ); | |
| 162 | } | |
| 163 | } | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Returns true if the list of files being listened to for changes contains | |
| 168 | * the given file. | |
| 169 | * | |
| 170 | * @param file Path to a system file. | |
| 171 | * @return true The given file is being monitored for changes. | |
| 172 | */ | |
| 173 | private boolean isListening( final Path file ) { | |
| 174 | return getEavesdropped().contains( file ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Returns a path for a given watch key. | |
| 179 | * | |
| 180 | * @param key The key to lookup its corresponding path. | |
| 181 | * @return The path for the given key. | |
| 182 | */ | |
| 183 | private Path get( final WatchKey key ) { | |
| 184 | return getWatchMap().get( key ); | |
| 185 | } | |
| 186 | ||
| 187 | private synchronized Map<WatchKey, Path> getWatchMap() { | |
| 188 | if( this.keys == null ) { | |
| 189 | this.keys = createWatchKeys(); | |
| 190 | } | |
| 191 | ||
| 192 | return this.keys; | |
| 193 | } | |
| 194 | ||
| 195 | protected Map<WatchKey, Path> createWatchKeys() { | |
| 196 | return new ConcurrentHashMap<>(); | |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * Returns a list of files that, when changed, will kick off a notification. | |
| 201 | * | |
| 202 | * @return A non-null, possibly empty, list of files. | |
| 203 | */ | |
| 204 | private synchronized Set<Path> getEavesdropped() { | |
| 205 | if( this.eavesdropped == null ) { | |
| 206 | this.eavesdropped = createEavesdropped(); | |
| 207 | } | |
| 208 | ||
| 209 | return this.eavesdropped; | |
| 210 | } | |
| 211 | ||
| 212 | protected Set<Path> createEavesdropped() { | |
| 213 | return ConcurrentHashMap.newKeySet(); | |
| 214 | } | |
| 215 | ||
| 216 | /** | |
| 217 | * The existing watch service, or a new instance if null. | |
| 218 | * | |
| 219 | * @return A valid WatchService instance, never null. | |
| 220 | * @throws IOException Could not create a new watch service. | |
| 221 | */ | |
| 222 | private synchronized WatchService getWatchService() throws IOException { | |
| 223 | if( this.watchService == null ) { | |
| 224 | this.watchService = createWatchService(); | |
| 225 | } | |
| 226 | ||
| 227 | return this.watchService; | |
| 228 | } | |
| 229 | ||
| 230 | protected WatchService createWatchService() throws IOException { | |
| 231 | final FileSystem fileSystem = FileSystems.getDefault(); | |
| 232 | return fileSystem.newWatchService(); | |
| 233 | } | |
| 234 | ||
| 235 | /** | |
| 236 | * Answers whether the loop should continue executing. | |
| 237 | * | |
| 238 | * @return true The internal listening loop should continue listening for file | |
| 239 | * modification events. | |
| 240 | */ | |
| 241 | protected boolean isListening() { | |
| 242 | return this.listening; | |
| 243 | } | |
| 244 | ||
| 245 | /** | |
| 246 | * Requests the snitch to stop eavesdropping on file changes. | |
| 247 | * | |
| 248 | * @param listening Use true to indicate the service should stop running. | |
| 249 | */ | |
| 250 | private void setListening( final boolean listening ) { | |
| 251 | this.listening = listening; | |
| 252 | } | |
| 253 | } | |
| 1 | 254 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.sigils; | |
| 29 | ||
| 30 | import static com.keenwrite.sigils.YamlSigilOperator.KEY_SEPARATOR_DEF; | |
| 31 | ||
| 32 | /** | |
| 33 | * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils. | |
| 34 | */ | |
| 35 | public class RSigilOperator extends SigilOperator { | |
| 36 | public static final char KEY_SEPARATOR_R = '$'; | |
| 37 | ||
| 38 | public static final String PREFIX = "`r#"; | |
| 39 | public static final char SUFFIX = '`'; | |
| 40 | ||
| 41 | private final String mDelimiterBegan = | |
| 42 | getUserPreferences().getRDelimiterBegan(); | |
| 43 | private final String mDelimiterEnded = | |
| 44 | getUserPreferences().getRDelimiterEnded(); | |
| 45 | ||
| 46 | /** | |
| 47 | * Returns the given string R-escaping backticks prepended and appended. This | |
| 48 | * is not null safe. Do not pass null into this method. | |
| 49 | * | |
| 50 | * @param key The string to adorn with R token delimiters. | |
| 51 | * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`". | |
| 52 | */ | |
| 53 | @Override | |
| 54 | public String apply( final String key ) { | |
| 55 | assert key != null; | |
| 56 | ||
| 57 | return PREFIX | |
| 58 | + mDelimiterBegan | |
| 59 | + entoken( key ) | |
| 60 | + mDelimiterEnded | |
| 61 | + SUFFIX; | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Transforms a definition key (bracketed by token delimiters) into the | |
| 66 | * expected format for an R variable key name. | |
| 67 | * | |
| 68 | * @param key The variable name to transform, can be empty but not null. | |
| 69 | * @return The transformed variable name. | |
| 70 | */ | |
| 71 | public static String entoken( final String key ) { | |
| 72 | return "v$" + | |
| 73 | YamlSigilOperator.detoken( key ) | |
| 74 | .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R ); | |
| 75 | } | |
| 76 | } | |
| 1 | 77 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.sigils; | |
| 29 | ||
| 30 | import com.keenwrite.preferences.UserPreferences; | |
| 31 | ||
| 32 | import java.util.function.UnaryOperator; | |
| 33 | ||
| 34 | /** | |
| 35 | * Responsible for updating definition keys to use a machine-readable format | |
| 36 | * corresponding to the type of file being edited. This changes a definition | |
| 37 | * key name based on some criteria determined by the factory that creates | |
| 38 | * implementations of this interface. | |
| 39 | */ | |
| 40 | public abstract class SigilOperator implements UnaryOperator<String> { | |
| 41 | protected static UserPreferences getUserPreferences() { | |
| 42 | return UserPreferences.getInstance(); | |
| 43 | } | |
| 44 | } | |
| 1 | 45 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.sigils; | |
| 29 | ||
| 30 | import java.util.regex.Pattern; | |
| 31 | ||
| 32 | import static java.lang.String.format; | |
| 33 | import static java.util.regex.Pattern.compile; | |
| 34 | import static java.util.regex.Pattern.quote; | |
| 35 | ||
| 36 | /** | |
| 37 | * Brackets definition keys with token delimiters. | |
| 38 | */ | |
| 39 | public class YamlSigilOperator extends SigilOperator { | |
| 40 | public static final char KEY_SEPARATOR_DEF = '.'; | |
| 41 | ||
| 42 | private static final String mDelimiterBegan = | |
| 43 | getUserPreferences().getDefDelimiterBegan(); | |
| 44 | private static final String mDelimiterEnded = | |
| 45 | getUserPreferences().getDefDelimiterEnded(); | |
| 46 | ||
| 47 | /** | |
| 48 | * Non-greedy match of key names delimited by definition tokens. | |
| 49 | */ | |
| 50 | private static final String REGEX = | |
| 51 | format( "(%s.*?%s)", quote( mDelimiterBegan ), quote( mDelimiterEnded ) ); | |
| 52 | ||
| 53 | /** | |
| 54 | * Compiled regular expression for matching delimited references. | |
| 55 | */ | |
| 56 | public static final Pattern REGEX_PATTERN = compile( REGEX ); | |
| 57 | ||
| 58 | /** | |
| 59 | * Returns the given {@link String} verbatim because variables in YAML | |
| 60 | * documents and plain Markdown documents already have the appropriate | |
| 61 | * tokenizable syntax wrapped around the text. | |
| 62 | * | |
| 63 | * @param key Returned verbatim. | |
| 64 | */ | |
| 65 | @Override | |
| 66 | public String apply( final String key ) { | |
| 67 | return key; | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Adds delimiters to the given key. | |
| 72 | * | |
| 73 | * @param key The key to adorn with start and stop definition tokens. | |
| 74 | * @return The given key bracketed by definition token symbols. | |
| 75 | */ | |
| 76 | public static String entoken( final String key ) { | |
| 77 | assert key != null; | |
| 78 | return mDelimiterBegan + key + mDelimiterEnded; | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Removes start and stop definition key delimiters from the given key. This | |
| 83 | * method does not check for delimiters, only that there are sufficient | |
| 84 | * characters to remove from either end of the given key. | |
| 85 | * | |
| 86 | * @param key The key adorned with start and stop definition tokens. | |
| 87 | * @return The given key with the delimiters removed. | |
| 88 | */ | |
| 89 | public static String detoken( final String key ) { | |
| 90 | final int beganLen = mDelimiterBegan.length(); | |
| 91 | final int endedLen = mDelimiterEnded.length(); | |
| 92 | ||
| 93 | return key.length() > beganLen + endedLen | |
| 94 | ? key.substring( beganLen, key.length() - endedLen ) | |
| 95 | : key; | |
| 96 | } | |
| 97 | } | |
| 1 | 98 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.spelling.api; | |
| 29 | ||
| 30 | import java.util.function.BiConsumer; | |
| 31 | ||
| 32 | /** | |
| 33 | * Represents an operation that accepts two input arguments and returns no | |
| 34 | * result. Unlike most other functional interfaces, this class is expected to | |
| 35 | * operate via side-effects. | |
| 36 | * <p> | |
| 37 | * This is used instead of a {@link BiConsumer} to avoid autoboxing. | |
| 38 | * </p> | |
| 39 | */ | |
| 40 | @FunctionalInterface | |
| 41 | public interface SpellCheckListener { | |
| 42 | ||
| 43 | /** | |
| 44 | * Performs an operation on the given arguments. | |
| 45 | * | |
| 46 | * @param text The text associated with a beginning and ending offset. | |
| 47 | * @param beganOffset A starting offset, used as an index into a string. | |
| 48 | * @param endedOffset An ending offset, which should equal text.length() + | |
| 49 | * beganOffset. | |
| 50 | */ | |
| 51 | void accept( String text, int beganOffset, int endedOffset ); | |
| 52 | } | |
| 1 | 53 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.spelling.api; | |
| 29 | ||
| 30 | import java.util.List; | |
| 31 | ||
| 32 | /** | |
| 33 | * Defines the responsibilities for a spell checking API. The intention is | |
| 34 | * to allow different spell checking implementations to be used by the | |
| 35 | * application, such as SymSpell and LinSpell. | |
| 36 | */ | |
| 37 | public interface SpellChecker { | |
| 38 | ||
| 39 | /** | |
| 40 | * Answers whether the given lexeme, in whole, is found in the lexicon. The | |
| 41 | * lexicon lookup is performed case-insensitively. This method should be | |
| 42 | * used instead of {@link #suggestions(String, int)} for performance reasons. | |
| 43 | * | |
| 44 | * @param lexeme The word to check for correctness. | |
| 45 | * @return {@code true} if the lexeme is in the lexicon. | |
| 46 | */ | |
| 47 | boolean inLexicon( String lexeme ); | |
| 48 | ||
| 49 | /** | |
| 50 | * Gets a list of spelling corrections for the given lexeme. | |
| 51 | * | |
| 52 | * @param lexeme A word to check for correctness that's not in the lexicon. | |
| 53 | * @param count The maximum number of alternatives to return. | |
| 54 | * @return A list of words in the lexicon that are similar to the given | |
| 55 | * lexeme. | |
| 56 | */ | |
| 57 | List<String> suggestions( String lexeme, int count ); | |
| 58 | ||
| 59 | /** | |
| 60 | * Iterates over the given text, emitting starting and ending offsets into | |
| 61 | * the text for every word that is missing from the lexicon. | |
| 62 | * | |
| 63 | * @param text The text to check for words missing from the lexicon. | |
| 64 | * @param consumer Every missing word emits a message with the starting | |
| 65 | * and ending offset into the text where said word is found. | |
| 66 | */ | |
| 67 | void proofread( String text, SpellCheckListener consumer ); | |
| 68 | } | |
| 1 | 69 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.spelling.impl; | |
| 29 | ||
| 30 | import com.keenwrite.spelling.api.SpellCheckListener; | |
| 31 | import com.keenwrite.spelling.api.SpellChecker; | |
| 32 | ||
| 33 | import java.util.List; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for spell checking in the event that a real spell checking | |
| 37 | * implementation cannot be created (for any reason). Does not perform any | |
| 38 | * spell checking and indicates that any given lexeme is in the lexicon. | |
| 39 | */ | |
| 40 | public class PermissiveSpeller implements SpellChecker { | |
| 41 | /** | |
| 42 | * Returns {@code true}, ignoring the given word. | |
| 43 | * | |
| 44 | * @param ignored Unused. | |
| 45 | * @return {@code true} | |
| 46 | */ | |
| 47 | @Override | |
| 48 | public boolean inLexicon( final String ignored ) { | |
| 49 | return true; | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Returns an array with the given lexeme. | |
| 54 | * | |
| 55 | * @param lexeme The word to return. | |
| 56 | * @param ignored Unused. | |
| 57 | * @return A suggestion list containing the given lexeme. | |
| 58 | */ | |
| 59 | @Override | |
| 60 | public List<String> suggestions( final String lexeme, final int ignored ) { | |
| 61 | return List.of( lexeme ); | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Performs no action. | |
| 66 | * | |
| 67 | * @param text Unused. | |
| 68 | * @param ignored Uncalled. | |
| 69 | */ | |
| 70 | @Override | |
| 71 | public void proofread( | |
| 72 | final String text, final SpellCheckListener ignored ) { | |
| 73 | } | |
| 74 | } | |
| 1 | 75 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.spelling.impl; | |
| 29 | ||
| 30 | import com.keenwrite.spelling.api.SpellCheckListener; | |
| 31 | import com.keenwrite.spelling.api.SpellChecker; | |
| 32 | import io.gitlab.rxp90.jsymspell.SuggestItem; | |
| 33 | import io.gitlab.rxp90.jsymspell.SymSpell; | |
| 34 | import io.gitlab.rxp90.jsymspell.SymSpellBuilder; | |
| 35 | ||
| 36 | import java.text.BreakIterator; | |
| 37 | import java.util.ArrayList; | |
| 38 | import java.util.Collection; | |
| 39 | import java.util.List; | |
| 40 | ||
| 41 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity; | |
| 42 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL; | |
| 43 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST; | |
| 44 | import static java.lang.Character.isLetter; | |
| 45 | ||
| 46 | /** | |
| 47 | * Responsible for spell checking using {@link SymSpell}. | |
| 48 | */ | |
| 49 | public class SymSpellSpeller implements SpellChecker { | |
| 50 | private final BreakIterator mBreakIterator = BreakIterator.getWordInstance(); | |
| 51 | ||
| 52 | private final SymSpell mSymSpell; | |
| 53 | ||
| 54 | /** | |
| 55 | * Creates a new lexicon for the given collection of lexemes. | |
| 56 | * | |
| 57 | * @param lexiconWords The words in the lexicon to add for spell checking, | |
| 58 | * must not be empty. | |
| 59 | * @return An instance of {@link SpellChecker} that can check if a word | |
| 60 | * is correct and suggest alternatives. | |
| 61 | */ | |
| 62 | public static SpellChecker forLexicon( | |
| 63 | final Collection<String> lexiconWords ) { | |
| 64 | assert lexiconWords != null && !lexiconWords.isEmpty(); | |
| 65 | ||
| 66 | final SymSpellBuilder builder = new SymSpellBuilder() | |
| 67 | .setLexiconWords( lexiconWords ); | |
| 68 | ||
| 69 | return new SymSpellSpeller( builder.build() ); | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Prevent direct instantiation so that only the {@link SpellChecker} | |
| 74 | * interface | |
| 75 | * is available. | |
| 76 | * | |
| 77 | * @param symSpell The implementation-specific spell checker. | |
| 78 | */ | |
| 79 | private SymSpellSpeller( final SymSpell symSpell ) { | |
| 80 | mSymSpell = symSpell; | |
| 81 | } | |
| 82 | ||
| 83 | @Override | |
| 84 | public boolean inLexicon( final String lexeme ) { | |
| 85 | return lookup( lexeme, CLOSEST ).size() == 1; | |
| 86 | } | |
| 87 | ||
| 88 | @Override | |
| 89 | public List<String> suggestions( final String lexeme, int count ) { | |
| 90 | final List<String> result = new ArrayList<>( count ); | |
| 91 | ||
| 92 | for( final var item : lookup( lexeme, ALL ) ) { | |
| 93 | if( count-- > 0 ) { | |
| 94 | result.add( item.getSuggestion() ); | |
| 95 | } | |
| 96 | else { | |
| 97 | break; | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | return result; | |
| 102 | } | |
| 103 | ||
| 104 | @Override | |
| 105 | public void proofread( | |
| 106 | final String text, final SpellCheckListener consumer ) { | |
| 107 | assert text != null; | |
| 108 | assert consumer != null; | |
| 109 | ||
| 110 | mBreakIterator.setText( text ); | |
| 111 | ||
| 112 | int boundaryIndex = mBreakIterator.first(); | |
| 113 | int previousIndex = 0; | |
| 114 | ||
| 115 | while( boundaryIndex != BreakIterator.DONE ) { | |
| 116 | final var lex = text.substring( previousIndex, boundaryIndex ) | |
| 117 | .toLowerCase(); | |
| 118 | ||
| 119 | // Get the lexeme for the possessive. | |
| 120 | final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" ); | |
| 121 | final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex; | |
| 122 | ||
| 123 | if( isWord( lexeme ) && !inLexicon( lexeme ) ) { | |
| 124 | consumer.accept( lex, previousIndex, boundaryIndex ); | |
| 125 | } | |
| 126 | ||
| 127 | previousIndex = boundaryIndex; | |
| 128 | boundaryIndex = mBreakIterator.next(); | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * Answers whether the given string is likely a word by checking the first | |
| 134 | * character. | |
| 135 | * | |
| 136 | * @param word The word to check. | |
| 137 | * @return {@code true} if the word begins with a letter. | |
| 138 | */ | |
| 139 | private boolean isWord( final String word ) { | |
| 140 | return !word.isEmpty() && isLetter( word.charAt( 0 ) ); | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Returns a list of {@link SuggestItem} instances that provide alternative | |
| 145 | * spellings for the given lexeme. | |
| 146 | * | |
| 147 | * @param lexeme A word to look up in the lexicon. | |
| 148 | * @param v Influences the number of results returned. | |
| 149 | * @return Alternative lexemes. | |
| 150 | */ | |
| 151 | private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) { | |
| 152 | return getSpeller().lookup( lexeme, v ); | |
| 153 | } | |
| 154 | ||
| 155 | private SymSpell getSpeller() { | |
| 156 | return mSymSpell; | |
| 157 | } | |
| 158 | } | |
| 1 | 159 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.util; | |
| 28 | ||
| 29 | import de.jensd.fx.glyphs.GlyphIcons; | |
| 30 | import javafx.beans.value.ObservableBooleanValue; | |
| 31 | import javafx.event.ActionEvent; | |
| 32 | import javafx.event.EventHandler; | |
| 33 | import javafx.scene.input.KeyCombination; | |
| 34 | ||
| 35 | /** | |
| 36 | * Defines actions the user can take by interacting with the GUI. | |
| 37 | */ | |
| 38 | public class Action { | |
| 39 | public final String text; | |
| 40 | public final KeyCombination accelerator; | |
| 41 | public final GlyphIcons icon; | |
| 42 | public final EventHandler<ActionEvent> action; | |
| 43 | public final ObservableBooleanValue disable; | |
| 44 | ||
| 45 | public Action( | |
| 46 | final String text, | |
| 47 | final String accelerator, | |
| 48 | final GlyphIcons icon, | |
| 49 | final EventHandler<ActionEvent> action, | |
| 50 | final ObservableBooleanValue disable ) { | |
| 51 | ||
| 52 | this.text = text; | |
| 53 | this.accelerator = accelerator == null ? | |
| 54 | null : KeyCombination.valueOf( accelerator ); | |
| 55 | this.icon = icon; | |
| 56 | this.action = action; | |
| 57 | this.disable = disable; | |
| 58 | } | |
| 59 | } | |
| 1 | 60 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | import com.keenwrite.Messages; | |
| 31 | import de.jensd.fx.glyphs.GlyphIcons; | |
| 32 | import javafx.beans.value.ObservableBooleanValue; | |
| 33 | import javafx.event.ActionEvent; | |
| 34 | import javafx.event.EventHandler; | |
| 35 | ||
| 36 | /** | |
| 37 | * Provides a fluent interface around constructing actions so that duplication | |
| 38 | * can be avoided. | |
| 39 | */ | |
| 40 | public class ActionBuilder { | |
| 41 | private String mText; | |
| 42 | private String mAccelerator; | |
| 43 | private GlyphIcons mIcon; | |
| 44 | private EventHandler<ActionEvent> mAction; | |
| 45 | private ObservableBooleanValue mDisable; | |
| 46 | ||
| 47 | /** | |
| 48 | * Sets the action text based on a resource bundle key. | |
| 49 | * | |
| 50 | * @param key The key to look up in the {@link Messages}. | |
| 51 | * @return The corresponding value, or the key name if none found. | |
| 52 | */ | |
| 53 | public ActionBuilder setText( final String key ) { | |
| 54 | mText = Messages.get( key, key ); | |
| 55 | return this; | |
| 56 | } | |
| 57 | ||
| 58 | public ActionBuilder setAccelerator( final String accelerator ) { | |
| 59 | mAccelerator = accelerator; | |
| 60 | return this; | |
| 61 | } | |
| 62 | ||
| 63 | public ActionBuilder setIcon( final GlyphIcons icon ) { | |
| 64 | mIcon = icon; | |
| 65 | return this; | |
| 66 | } | |
| 67 | ||
| 68 | public ActionBuilder setAction( final EventHandler<ActionEvent> action ) { | |
| 69 | mAction = action; | |
| 70 | return this; | |
| 71 | } | |
| 72 | ||
| 73 | public ActionBuilder setDisable( final ObservableBooleanValue disable ) { | |
| 74 | mDisable = disable; | |
| 75 | return this; | |
| 76 | } | |
| 77 | ||
| 78 | public Action build() { | |
| 79 | return new Action( mText, mAccelerator, mIcon, mAction, mDisable ); | |
| 80 | } | |
| 81 | } | |
| 1 | 82 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.util; | |
| 28 | ||
| 29 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 30 | import javafx.scene.Node; | |
| 31 | import javafx.scene.control.Button; | |
| 32 | import javafx.scene.control.Menu; | |
| 33 | import javafx.scene.control.MenuItem; | |
| 34 | import javafx.scene.control.Separator; | |
| 35 | import javafx.scene.control.SeparatorMenuItem; | |
| 36 | import javafx.scene.control.ToolBar; | |
| 37 | import javafx.scene.control.Tooltip; | |
| 38 | ||
| 39 | /** | |
| 40 | * Responsible for creating menu items and toolbar buttons. | |
| 41 | */ | |
| 42 | public class ActionUtils { | |
| 43 | ||
| 44 | public static Menu createMenu( final String text, final Action... actions ) { | |
| 45 | return new Menu( text, null, createMenuItems( actions ) ); | |
| 46 | } | |
| 47 | ||
| 48 | public static MenuItem[] createMenuItems( final Action... actions ) { | |
| 49 | final MenuItem[] menuItems = new MenuItem[ actions.length ]; | |
| 50 | ||
| 51 | for( int i = 0; i < actions.length; i++ ) { | |
| 52 | menuItems[ i ] = (actions[ i ] == null) | |
| 53 | ? new SeparatorMenuItem() | |
| 54 | : createMenuItem( actions[ i ] ); | |
| 55 | } | |
| 56 | ||
| 57 | return menuItems; | |
| 58 | } | |
| 59 | ||
| 60 | public static MenuItem createMenuItem( final Action action ) { | |
| 61 | final MenuItem menuItem = new MenuItem( action.text ); | |
| 62 | ||
| 63 | if( action.accelerator != null ) { | |
| 64 | menuItem.setAccelerator( action.accelerator ); | |
| 65 | } | |
| 66 | ||
| 67 | if( action.icon != null ) { | |
| 68 | menuItem.setGraphic( | |
| 69 | FontAwesomeIconFactory.get().createIcon( action.icon ) ); | |
| 70 | } | |
| 71 | ||
| 72 | menuItem.setOnAction( action.action ); | |
| 73 | ||
| 74 | if( action.disable != null ) { | |
| 75 | menuItem.disableProperty().bind( action.disable ); | |
| 76 | } | |
| 77 | ||
| 78 | menuItem.setMnemonicParsing( true ); | |
| 79 | ||
| 80 | return menuItem; | |
| 81 | } | |
| 82 | ||
| 83 | public static ToolBar createToolBar( final Action... actions ) { | |
| 84 | return new ToolBar( createToolBarButtons( actions ) ); | |
| 85 | } | |
| 86 | ||
| 87 | public static Node[] createToolBarButtons( final Action... actions ) { | |
| 88 | Node[] buttons = new Node[ actions.length ]; | |
| 89 | for( int i = 0; i < actions.length; i++ ) { | |
| 90 | buttons[ i ] = (actions[ i ] != null) | |
| 91 | ? createToolBarButton( actions[ i ] ) | |
| 92 | : new Separator(); | |
| 93 | } | |
| 94 | return buttons; | |
| 95 | } | |
| 96 | ||
| 97 | public static Button createToolBarButton( final Action action ) { | |
| 98 | final Button button = new Button(); | |
| 99 | button.setGraphic( | |
| 100 | FontAwesomeIconFactory | |
| 101 | .get() | |
| 102 | .createIcon( action.icon, "1.2em" ) ); | |
| 103 | ||
| 104 | String tooltip = action.text; | |
| 105 | ||
| 106 | if( tooltip.endsWith( "..." ) ) { | |
| 107 | tooltip = tooltip.substring( 0, tooltip.length() - 3 ); | |
| 108 | } | |
| 109 | ||
| 110 | if( action.accelerator != null ) { | |
| 111 | tooltip += " (" + action.accelerator.getDisplayText() + ')'; | |
| 112 | } | |
| 113 | ||
| 114 | button.setTooltip( new Tooltip( tooltip ) ); | |
| 115 | button.setFocusTraversable( false ); | |
| 116 | button.setOnAction( action.action ); | |
| 117 | ||
| 118 | if( action.disable != null ) { | |
| 119 | button.disableProperty().bind( action.disable ); | |
| 120 | } | |
| 121 | ||
| 122 | return button; | |
| 123 | } | |
| 124 | } | |
| 1 | 125 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | import java.util.LinkedHashMap; | |
| 31 | import java.util.Map; | |
| 32 | ||
| 33 | /** | |
| 34 | * A map that removes the oldest entry once its capacity (cache size) has | |
| 35 | * been reached. | |
| 36 | * | |
| 37 | * @param <K> The type of key mapped to a value. | |
| 38 | * @param <V> The type of value mapped to a key. | |
| 39 | */ | |
| 40 | public class BoundedCache<K, V> extends LinkedHashMap<K, V> { | |
| 41 | private final int mCacheSize; | |
| 42 | ||
| 43 | /** | |
| 44 | * Constructs a new instance having a finite size. | |
| 45 | * | |
| 46 | * @param cacheSize The maximum number of entries. | |
| 47 | */ | |
| 48 | public BoundedCache( final int cacheSize ) { | |
| 49 | mCacheSize = cacheSize; | |
| 50 | } | |
| 51 | ||
| 52 | @Override | |
| 53 | protected boolean removeEldestEntry( | |
| 54 | final Map.Entry<K, V> eldest ) { | |
| 55 | return size() > mCacheSize; | |
| 56 | } | |
| 57 | } | |
| 1 | 58 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.net.MalformedURLException; | |
| 32 | import java.net.URI; | |
| 33 | import java.net.URL; | |
| 34 | ||
| 35 | import static com.keenwrite.util.ProtocolScheme.UNKNOWN; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for determining the protocol of a resource. | |
| 39 | */ | |
| 40 | public class ProtocolResolver { | |
| 41 | /** | |
| 42 | * Returns the protocol for a given URI or filename. | |
| 43 | * | |
| 44 | * @param resource Determine the protocol for this URI or filename. | |
| 45 | * @return The protocol for the given resource. | |
| 46 | */ | |
| 47 | public static ProtocolScheme getProtocol( final String resource ) { | |
| 48 | String protocol; | |
| 49 | ||
| 50 | try { | |
| 51 | final URI uri = new URI( resource ); | |
| 52 | ||
| 53 | if( uri.isAbsolute() ) { | |
| 54 | protocol = uri.getScheme(); | |
| 55 | } | |
| 56 | else { | |
| 57 | final URL url = new URL( resource ); | |
| 58 | protocol = url.getProtocol(); | |
| 59 | } | |
| 60 | } catch( final Exception e ) { | |
| 61 | // Could be HTTP, HTTPS? | |
| 62 | if( resource.startsWith( "//" ) ) { | |
| 63 | throw new IllegalArgumentException( "Relative context: " + resource ); | |
| 64 | } | |
| 65 | else { | |
| 66 | final File file = new File( resource ); | |
| 67 | protocol = getProtocol( file ); | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | return ProtocolScheme.valueFrom( protocol ); | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Returns the protocol for a given file. | |
| 76 | * | |
| 77 | * @param file Determine the protocol for this file. | |
| 78 | * @return The protocol for the given file. | |
| 79 | */ | |
| 80 | private static String getProtocol( final File file ) { | |
| 81 | String result; | |
| 82 | ||
| 83 | try { | |
| 84 | result = file.toURI().toURL().getProtocol(); | |
| 85 | } catch( final MalformedURLException ex ) { | |
| 86 | // Value guaranteed to avoid identification as a standard protocol. | |
| 87 | result = UNKNOWN.toString(); | |
| 88 | } | |
| 89 | ||
| 90 | return result; | |
| 91 | } | |
| 92 | } | |
| 1 | 93 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents the type of data encoding scheme used for a universal resource | |
| 32 | * indicator. | |
| 33 | */ | |
| 34 | public enum ProtocolScheme { | |
| 35 | /** | |
| 36 | * Denotes either HTTP or HTTPS. | |
| 37 | */ | |
| 38 | HTTP, | |
| 39 | /** | |
| 40 | * Denotes a local file. | |
| 41 | */ | |
| 42 | FILE, | |
| 43 | /** | |
| 44 | * Could not determine schema (or is not supported by the application). | |
| 45 | */ | |
| 46 | UNKNOWN; | |
| 47 | ||
| 48 | /** | |
| 49 | * Answers {@code true} if the given protocol is either HTTP or HTTPS. | |
| 50 | * | |
| 51 | * @return {@code true} the protocol is either HTTP or HTTPS. | |
| 52 | */ | |
| 53 | public boolean isHttp() { | |
| 54 | return this == HTTP; | |
| 55 | } | |
| 56 | ||
| 57 | /** | |
| 58 | * Answers {@code true} if the given protocol is for a local file. | |
| 59 | * | |
| 60 | * @return {@code true} the protocol is for a local file reference. | |
| 61 | */ | |
| 62 | public boolean isFile() { | |
| 63 | return this == FILE; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * Determines the protocol scheme for a given string. | |
| 68 | * | |
| 69 | * @param protocol A string representing data encoding protocol scheme. | |
| 70 | * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a | |
| 71 | * valid value from this enumeration. | |
| 72 | */ | |
| 73 | public static ProtocolScheme valueFrom( String protocol ) { | |
| 74 | ProtocolScheme result = UNKNOWN; | |
| 75 | protocol = sanitize( protocol ); | |
| 76 | ||
| 77 | for( final var scheme : values() ) { | |
| 78 | // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate. | |
| 79 | if( protocol.startsWith( scheme.name() ) ) { | |
| 80 | result = scheme; | |
| 81 | break; | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 85 | return result; | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Returns an empty string if the given string to sanitize is {@code null}, | |
| 90 | * otherwise the given string in uppercase. Uppercase is used to align with | |
| 91 | * the enum name. | |
| 92 | * | |
| 93 | * @param s The string to sanitize, may be {@code null}. | |
| 94 | * @return A non-{@code null} string. | |
| 95 | */ | |
| 96 | private static String sanitize( final String s ) { | |
| 97 | return s == null ? "" : s.toUpperCase(); | |
| 98 | } | |
| 99 | } | |
| 1 | 100 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.util; | |
| 29 | ||
| 30 | import java.io.IOException; | |
| 31 | import java.net.URISyntaxException; | |
| 32 | import java.nio.file.*; | |
| 33 | import java.util.function.Consumer; | |
| 34 | ||
| 35 | import static java.nio.file.FileSystems.newFileSystem; | |
| 36 | import static java.util.Collections.emptyMap; | |
| 37 | ||
| 38 | /** | |
| 39 | * Responsible for finding file resources. | |
| 40 | */ | |
| 41 | public class ResourceWalker { | |
| 42 | private static final PathMatcher PATH_MATCHER = | |
| 43 | FileSystems.getDefault().getPathMatcher( "glob:**.{ttf,otf}" ); | |
| 44 | ||
| 45 | /** | |
| 46 | * @param dirName The root directory to scan for files matching the glob. | |
| 47 | * @param c The consumer function to call for each matching path found. | |
| 48 | * @throws URISyntaxException Could not convert the resource to a URI. | |
| 49 | * @throws IOException Could not walk the tree. | |
| 50 | */ | |
| 51 | public static void walk( final String dirName, final Consumer<Path> c ) | |
| 52 | throws URISyntaxException, IOException { | |
| 53 | final var resource = ResourceWalker.class.getResource( dirName ); | |
| 54 | ||
| 55 | if( resource != null ) { | |
| 56 | final var uri = resource.toURI(); | |
| 57 | final var path = uri.getScheme().equals( "jar" ) | |
| 58 | ? newFileSystem( uri, emptyMap() ).getPath( dirName ) | |
| 59 | : Paths.get( uri ); | |
| 60 | final var walk = Files.walk( path, 10 ); | |
| 61 | ||
| 62 | for( final var it = walk.iterator(); it.hasNext(); ) { | |
| 63 | final Path p = it.next(); | |
| 64 | if( PATH_MATCHER.matches( p ) ) { | |
| 65 | c.accept( p ); | |
| 66 | } | |
| 67 | } | |
| 68 | } | |
| 69 | } | |
| 70 | } | |
| 1 | 71 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.util; | |
| 28 | ||
| 29 | import java.util.prefs.Preferences; | |
| 30 | ||
| 31 | import javafx.application.Platform; | |
| 32 | import javafx.scene.shape.Rectangle; | |
| 33 | import javafx.stage.Stage; | |
| 34 | import javafx.stage.WindowEvent; | |
| 35 | ||
| 36 | /** | |
| 37 | * Saves and restores Stage state (window bounds, maximized, fullScreen). | |
| 38 | */ | |
| 39 | public class StageState { | |
| 40 | ||
| 41 | public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition"; | |
| 42 | public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor"; | |
| 43 | public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview"; | |
| 44 | ||
| 45 | private final Stage mStage; | |
| 46 | private final Preferences mState; | |
| 47 | ||
| 48 | private Rectangle normalBounds; | |
| 49 | private boolean runLaterPending; | |
| 50 | ||
| 51 | public StageState( final Stage stage, final Preferences state ) { | |
| 52 | mStage = stage; | |
| 53 | mState = state; | |
| 54 | ||
| 55 | restore(); | |
| 56 | ||
| 57 | stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() ); | |
| 58 | ||
| 59 | stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 60 | stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 61 | stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 62 | stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 63 | } | |
| 64 | ||
| 65 | private void save() { | |
| 66 | final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds; | |
| 67 | ||
| 68 | if( bounds != null ) { | |
| 69 | mState.putDouble( "windowX", bounds.getX() ); | |
| 70 | mState.putDouble( "windowY", bounds.getY() ); | |
| 71 | mState.putDouble( "windowWidth", bounds.getWidth() ); | |
| 72 | mState.putDouble( "windowHeight", bounds.getHeight() ); | |
| 73 | } | |
| 74 | ||
| 75 | mState.putBoolean( "windowMaximized", mStage.isMaximized() ); | |
| 76 | mState.putBoolean( "windowFullScreen", mStage.isFullScreen() ); | |
| 77 | } | |
| 78 | ||
| 79 | private void restore() { | |
| 80 | final double x = mState.getDouble( "windowX", Double.NaN ); | |
| 81 | final double y = mState.getDouble( "windowY", Double.NaN ); | |
| 82 | final double w = mState.getDouble( "windowWidth", Double.NaN ); | |
| 83 | final double h = mState.getDouble( "windowHeight", Double.NaN ); | |
| 84 | final boolean maximized = mState.getBoolean( "windowMaximized", false ); | |
| 85 | final boolean fullScreen = mState.getBoolean( "windowFullScreen", false ); | |
| 86 | ||
| 87 | if( !Double.isNaN( x ) && !Double.isNaN( y ) ) { | |
| 88 | mStage.setX( x ); | |
| 89 | mStage.setY( y ); | |
| 90 | } // else: default behavior is center on screen | |
| 91 | ||
| 92 | if( !Double.isNaN( w ) && !Double.isNaN( h ) ) { | |
| 93 | mStage.setWidth( w ); | |
| 94 | mStage.setHeight( h ); | |
| 95 | } // else: default behavior is use scene size | |
| 96 | ||
| 97 | if( fullScreen != mStage.isFullScreen() ) { | |
| 98 | mStage.setFullScreen( fullScreen ); | |
| 99 | } | |
| 100 | ||
| 101 | if( maximized != mStage.isMaximized() ) { | |
| 102 | mStage.setMaximized( maximized ); | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Remembers the window bounds when the window is not iconified, maximized or | |
| 108 | * in fullScreen. | |
| 109 | */ | |
| 110 | private void boundsChanged() { | |
| 111 | // avoid too many (and useless) runLater() invocations | |
| 112 | if( runLaterPending ) { | |
| 113 | return; | |
| 114 | } | |
| 115 | ||
| 116 | runLaterPending = true; | |
| 117 | ||
| 118 | // must use runLater() to ensure that change of all properties | |
| 119 | // (x, y, width, height, iconified, maximized and fullScreen) | |
| 120 | // has finished | |
| 121 | Platform.runLater( () -> { | |
| 122 | runLaterPending = false; | |
| 123 | ||
| 124 | if( isNormalState() ) { | |
| 125 | normalBounds = getStageBounds(); | |
| 126 | } | |
| 127 | } ); | |
| 128 | } | |
| 129 | ||
| 130 | private boolean isNormalState() { | |
| 131 | return !mStage.isIconified() && | |
| 132 | !mStage.isMaximized() && | |
| 133 | !mStage.isFullScreen(); | |
| 134 | } | |
| 135 | ||
| 136 | private Rectangle getStageBounds() { | |
| 137 | return new Rectangle( | |
| 138 | mStage.getX(), | |
| 139 | mStage.getY(), | |
| 140 | mStage.getWidth(), | |
| 141 | mStage.getHeight() | |
| 142 | ); | |
| 143 | } | |
| 144 | } | |
| 1 | 145 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.keenwrite.util; | |
| 28 | ||
| 29 | import java.util.ArrayList; | |
| 30 | import java.util.prefs.Preferences; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for trimming, storing, and retrieving strings. | |
| 34 | */ | |
| 35 | public class Utils { | |
| 36 | ||
| 37 | public static String ltrim( final String s ) { | |
| 38 | int i = 0; | |
| 39 | ||
| 40 | while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) { | |
| 41 | i++; | |
| 42 | } | |
| 43 | ||
| 44 | return s.substring( i ); | |
| 45 | } | |
| 46 | ||
| 47 | public static String rtrim( final String s ) { | |
| 48 | int i = s.length() - 1; | |
| 49 | ||
| 50 | while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) { | |
| 51 | i--; | |
| 52 | } | |
| 53 | ||
| 54 | return s.substring( 0, i + 1 ); | |
| 55 | } | |
| 56 | ||
| 57 | public static String[] getPrefsStrings( final Preferences prefs, | |
| 58 | String key ) { | |
| 59 | final ArrayList<String> arr = new ArrayList<>( 256 ); | |
| 60 | ||
| 61 | for( int i = 0; i < 10000; i++ ) { | |
| 62 | final String s = prefs.get( key + (i + 1), null ); | |
| 63 | ||
| 64 | if( s == null ) { | |
| 65 | break; | |
| 66 | } | |
| 67 | ||
| 68 | arr.add( s ); | |
| 69 | } | |
| 70 | ||
| 71 | return arr.toArray( new String[ 0 ] ); | |
| 72 | } | |
| 73 | ||
| 74 | public static void putPrefsStrings( Preferences prefs, String key, | |
| 75 | String[] strings ) { | |
| 76 | for( int i = 0; i < strings.length; i++ ) { | |
| 77 | prefs.put( key + (i + 1), strings[ i ] ); | |
| 78 | } | |
| 79 | ||
| 80 | for( int i = strings.length; prefs.get( key + (i + 1), | |
| 81 | null ) != null; i++ ) { | |
| 82 | prefs.remove( key + (i + 1) ); | |
| 83 | } | |
| 84 | } | |
| 85 | } | |
| 1 | 86 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import com.scrivenvar.service.Settings; | |
| 31 | import com.scrivenvar.util.ProtocolScheme; | |
| 32 | ||
| 33 | import java.nio.file.Path; | |
| 34 | ||
| 35 | import static com.scrivenvar.Constants.GLOB_PREFIX_FILE; | |
| 36 | import static com.scrivenvar.Constants.SETTINGS; | |
| 37 | import static com.scrivenvar.FileType.UNKNOWN; | |
| 38 | import static com.scrivenvar.predicates.PredicateFactory.createFileTypePredicate; | |
| 39 | import static java.lang.String.format; | |
| 40 | ||
| 41 | /** | |
| 42 | * Provides common behaviours for factories that instantiate classes based on | |
| 43 | * file type. | |
| 44 | */ | |
| 45 | public class AbstractFileFactory { | |
| 46 | ||
| 47 | private static final String MSG_UNKNOWN_FILE_TYPE = | |
| 48 | "Unknown type '%s' for file '%s'."; | |
| 49 | ||
| 50 | /** | |
| 51 | * Determines the file type from the path extension. This should only be | |
| 52 | * called when it is known that the file type won't be a definition file | |
| 53 | * (e.g., YAML or other definition source), but rather an editable file | |
| 54 | * (e.g., Markdown, XML, etc.). | |
| 55 | * | |
| 56 | * @param path The path with a file name extension. | |
| 57 | * @return The FileType for the given path. | |
| 58 | */ | |
| 59 | public FileType lookup( final Path path ) { | |
| 60 | return lookup( path, GLOB_PREFIX_FILE ); | |
| 61 | } | |
| 62 | ||
| 63 | /** | |
| 64 | * Creates a file type that corresponds to the given path. | |
| 65 | * | |
| 66 | * @param path Reference to a variable definition file. | |
| 67 | * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE. | |
| 68 | * @return The file type that corresponds to the given path. | |
| 69 | */ | |
| 70 | protected FileType lookup( final Path path, final String prefix ) { | |
| 71 | assert path != null; | |
| 72 | assert prefix != null; | |
| 73 | ||
| 74 | final var settings = getSettings(); | |
| 75 | final var keys = settings.getKeys( prefix ); | |
| 76 | ||
| 77 | var found = false; | |
| 78 | var fileType = UNKNOWN; | |
| 79 | ||
| 80 | while( keys.hasNext() && !found ) { | |
| 81 | final var key = keys.next(); | |
| 82 | final var patterns = settings.getStringSettingList( key ); | |
| 83 | final var predicate = createFileTypePredicate( patterns ); | |
| 84 | ||
| 85 | if( found = predicate.test( path.toFile() ) ) { | |
| 86 | // Remove the EXTENSIONS_PREFIX to get the filename extension mapped | |
| 87 | // to a standard name (as defined in the settings.properties file). | |
| 88 | final String suffix = key.replace( prefix + ".", "" ); | |
| 89 | fileType = FileType.from( suffix ); | |
| 90 | } | |
| 91 | } | |
| 92 | ||
| 93 | return fileType; | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Throws IllegalArgumentException because the given path could not be | |
| 98 | * recognized. This exists because | |
| 99 | * | |
| 100 | * @param type The detected path type (protocol, file extension, etc.). | |
| 101 | * @param path The path to a source of definitions. | |
| 102 | */ | |
| 103 | protected void unknownFileType( | |
| 104 | final ProtocolScheme type, final String path ) { | |
| 105 | final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path ); | |
| 106 | throw new IllegalArgumentException( msg ); | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * Return the singleton Settings instance. | |
| 111 | * | |
| 112 | * @return A non-null instance. | |
| 113 | */ | |
| 114 | private Settings getSettings() { | |
| 115 | return SETTINGS; | |
| 116 | } | |
| 117 | } | |
| 118 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import com.scrivenvar.service.Settings; | |
| 31 | ||
| 32 | import java.nio.file.Path; | |
| 33 | import java.nio.file.Paths; | |
| 34 | ||
| 35 | /** | |
| 36 | * Defines application-wide default values. | |
| 37 | */ | |
| 38 | public class Constants { | |
| 39 | ||
| 40 | public static final Settings SETTINGS = Services.load( Settings.class ); | |
| 41 | ||
| 42 | /** | |
| 43 | * Prevent instantiation. | |
| 44 | */ | |
| 45 | private Constants() { | |
| 46 | } | |
| 47 | ||
| 48 | private static String get( final String key ) { | |
| 49 | return SETTINGS.getSetting( key, "" ); | |
| 50 | } | |
| 51 | ||
| 52 | @SuppressWarnings("SameParameterValue") | |
| 53 | private static int get( final String key, final int defaultValue ) { | |
| 54 | return SETTINGS.getSetting( key, defaultValue ); | |
| 55 | } | |
| 56 | ||
| 57 | // Bootstrapping... | |
| 58 | public static final String SETTINGS_NAME = | |
| 59 | "/com/scrivenvar/settings.properties"; | |
| 60 | ||
| 61 | public static final String DEFINITION_NAME = "variables.yaml"; | |
| 62 | ||
| 63 | public static final String APP_TITLE = get( "application.title" ); | |
| 64 | public static final String APP_BUNDLE_NAME = get( "application.messages" ); | |
| 65 | ||
| 66 | // Prevent double events when updating files on Linux (save and timestamp). | |
| 67 | public static final int APP_WATCHDOG_TIMEOUT = get( | |
| 68 | "application.watchdog.timeout", 200 ); | |
| 69 | ||
| 70 | public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" ); | |
| 71 | public static final String STYLESHEET_MARKDOWN = get( | |
| 72 | "file.stylesheet.markdown" ); | |
| 73 | public static final String STYLESHEET_PREVIEW = get( | |
| 74 | "file.stylesheet.preview" ); | |
| 75 | ||
| 76 | public static final String FILE_LOGO_16 = get( "file.logo.16" ); | |
| 77 | public static final String FILE_LOGO_32 = get( "file.logo.32" ); | |
| 78 | public static final String FILE_LOGO_128 = get( "file.logo.128" ); | |
| 79 | public static final String FILE_LOGO_256 = get( "file.logo.256" ); | |
| 80 | public static final String FILE_LOGO_512 = get( "file.logo.512" ); | |
| 81 | ||
| 82 | public static final String PREFS_ROOT = get( "preferences.root" ); | |
| 83 | public static final String PREFS_STATE = get( "preferences.root.state" ); | |
| 84 | ||
| 85 | /** | |
| 86 | * Refer to filename extension settings in the configuration file. Do not | |
| 87 | * terminate these prefixes with a period. | |
| 88 | */ | |
| 89 | public static final String GLOB_PREFIX_FILE = "file.ext"; | |
| 90 | public static final String GLOB_PREFIX_DEFINITION = | |
| 91 | "definition." + GLOB_PREFIX_FILE; | |
| 92 | ||
| 93 | /** | |
| 94 | * Three parameters: line number, column number, and offset. | |
| 95 | */ | |
| 96 | public static final String STATUS_BAR_LINE = "Main.status.line"; | |
| 97 | ||
| 98 | public static final String STATUS_BAR_OK = "Main.status.state.default"; | |
| 99 | ||
| 100 | /** | |
| 101 | * Used to show an error while parsing, usually syntactical. | |
| 102 | */ | |
| 103 | public static final String STATUS_PARSE_ERROR = "Main.status.error.parse"; | |
| 104 | public static final String STATUS_DEFINITION_BLANK = "Main.status.error.def.blank"; | |
| 105 | public static final String STATUS_DEFINITION_EMPTY = "Main.status.error.def.empty"; | |
| 106 | ||
| 107 | /** | |
| 108 | * One parameter: the word under the cursor that could not be found. | |
| 109 | */ | |
| 110 | public static final String STATUS_DEFINITION_MISSING = "Main.status.error.def.missing"; | |
| 111 | ||
| 112 | /** | |
| 113 | * Used when creating flat maps relating to resolved variables. | |
| 114 | */ | |
| 115 | public static final int DEFAULT_MAP_SIZE = 64; | |
| 116 | ||
| 117 | /** | |
| 118 | * Default image extension order to use when scanning. | |
| 119 | */ | |
| 120 | public static final String PERSIST_IMAGES_DEFAULT = | |
| 121 | get( "file.ext.image.order" ); | |
| 122 | ||
| 123 | /** | |
| 124 | * Default working directory to use for R startup script. | |
| 125 | */ | |
| 126 | public static final String USER_DIRECTORY = System.getProperty( "user.dir" ); | |
| 127 | ||
| 128 | /** | |
| 129 | * Default path to use for an untitled (pathless) file. | |
| 130 | */ | |
| 131 | public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY ); | |
| 132 | ||
| 133 | /** | |
| 134 | * Default starting delimiter for definition variables. | |
| 135 | */ | |
| 136 | public static final String DEF_DELIM_BEGAN_DEFAULT = "${"; | |
| 137 | ||
| 138 | /** | |
| 139 | * Default ending delimiter for definition variables. | |
| 140 | */ | |
| 141 | public static final String DEF_DELIM_ENDED_DEFAULT = "}"; | |
| 142 | ||
| 143 | /** | |
| 144 | * Default starting delimiter when inserting R variables. | |
| 145 | */ | |
| 146 | public static final String R_DELIM_BEGAN_DEFAULT = "x( "; | |
| 147 | ||
| 148 | /** | |
| 149 | * Default ending delimiter when inserting R variables. | |
| 150 | */ | |
| 151 | public static final String R_DELIM_ENDED_DEFAULT = " )"; | |
| 152 | ||
| 153 | /** | |
| 154 | * Resource directory where different language lexicons are located. | |
| 155 | */ | |
| 156 | public static final String LEXICONS_DIRECTORY = "lexicons"; | |
| 157 | ||
| 158 | /** | |
| 159 | * Used as the prefix for uniquely identifying HTML block elements, which | |
| 160 | * helps coordinate scrolling the preview pane to where the user is typing. | |
| 161 | */ | |
| 162 | public static final String PARAGRAPH_ID_PREFIX = "p-"; | |
| 163 | ||
| 164 | /** | |
| 165 | * Absolute location of true type font files within the Java archive file. | |
| 166 | */ | |
| 167 | public static final String FONT_DIRECTORY = "/fonts"; | |
| 168 | ||
| 169 | /** | |
| 170 | * Default text editor font size, in points. | |
| 171 | */ | |
| 172 | public static final float FONT_SIZE_EDITOR = 12f; | |
| 173 | } | |
| 174 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * Redistribution and use in source and binary forms, with or without | |
| 5 | * modification, are permitted provided that the following conditions are met: | |
| 6 | * | |
| 7 | * o Redistributions of source code must retain the above copyright | |
| 8 | * notice, this list of conditions and the following disclaimer. | |
| 9 | * | |
| 10 | * o Redistributions in binary form must reproduce the above copyright | |
| 11 | * notice, this list of conditions and the following disclaimer in the | |
| 12 | * documentation and/or other materials provided with the distribution. | |
| 13 | * | |
| 14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 15 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 17 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 18 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 19 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 20 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 21 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 22 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 25 | */ | |
| 26 | package com.scrivenvar; | |
| 27 | ||
| 28 | import com.scrivenvar.editors.EditorPane; | |
| 29 | import com.scrivenvar.editors.markdown.MarkdownEditorPane; | |
| 30 | import com.scrivenvar.service.events.Notification; | |
| 31 | import com.scrivenvar.service.events.Notifier; | |
| 32 | import javafx.beans.binding.Bindings; | |
| 33 | import javafx.beans.property.BooleanProperty; | |
| 34 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 35 | import javafx.beans.property.ReadOnlyBooleanWrapper; | |
| 36 | import javafx.beans.property.SimpleBooleanProperty; | |
| 37 | import javafx.beans.value.ChangeListener; | |
| 38 | import javafx.event.Event; | |
| 39 | import javafx.event.EventHandler; | |
| 40 | import javafx.event.EventType; | |
| 41 | import javafx.scene.Scene; | |
| 42 | import javafx.scene.control.Tab; | |
| 43 | import javafx.scene.control.Tooltip; | |
| 44 | import javafx.scene.text.Text; | |
| 45 | import javafx.stage.Window; | |
| 46 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 47 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 48 | import org.fxmisc.undo.UndoManager; | |
| 49 | import org.jetbrains.annotations.NotNull; | |
| 50 | import org.mozilla.universalchardet.UniversalDetector; | |
| 51 | ||
| 52 | import java.io.File; | |
| 53 | import java.nio.charset.Charset; | |
| 54 | import java.nio.file.Files; | |
| 55 | import java.nio.file.Path; | |
| 56 | ||
| 57 | import static com.scrivenvar.Messages.get; | |
| 58 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 59 | import static com.scrivenvar.StatusBarNotifier.getNotifier; | |
| 60 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 61 | import static java.util.Locale.ENGLISH; | |
| 62 | import static javafx.application.Platform.runLater; | |
| 63 | ||
| 64 | /** | |
| 65 | * Editor for a single file. | |
| 66 | */ | |
| 67 | public final class FileEditorTab extends Tab { | |
| 68 | ||
| 69 | private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane(); | |
| 70 | ||
| 71 | private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper(); | |
| 72 | private final BooleanProperty canUndo = new SimpleBooleanProperty(); | |
| 73 | private final BooleanProperty canRedo = new SimpleBooleanProperty(); | |
| 74 | ||
| 75 | /** | |
| 76 | * Character encoding used by the file (or default encoding if none found). | |
| 77 | */ | |
| 78 | private Charset mEncoding = UTF_8; | |
| 79 | ||
| 80 | /** | |
| 81 | * File to load into the editor. | |
| 82 | */ | |
| 83 | private Path mPath; | |
| 84 | ||
| 85 | public FileEditorTab( final Path path ) { | |
| 86 | setPath( path ); | |
| 87 | ||
| 88 | mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() ); | |
| 89 | ||
| 90 | setOnSelectionChanged( e -> { | |
| 91 | if( isSelected() ) { | |
| 92 | runLater( this::activated ); | |
| 93 | requestFocus(); | |
| 94 | } | |
| 95 | } ); | |
| 96 | } | |
| 97 | ||
| 98 | private void updateTab() { | |
| 99 | setText( getTabTitle() ); | |
| 100 | setGraphic( getModifiedMark() ); | |
| 101 | setTooltip( getTabTooltip() ); | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Returns the base filename (without the directory names). | |
| 106 | * | |
| 107 | * @return The untitled text if the path hasn't been set. | |
| 108 | */ | |
| 109 | private String getTabTitle() { | |
| 110 | return getPath().getFileName().toString(); | |
| 111 | } | |
| 112 | ||
| 113 | /** | |
| 114 | * Returns the full filename represented by the path. | |
| 115 | * | |
| 116 | * @return The untitled text if the path hasn't been set. | |
| 117 | */ | |
| 118 | private Tooltip getTabTooltip() { | |
| 119 | final Path filePath = getPath(); | |
| 120 | return new Tooltip( filePath == null ? "" : filePath.toString() ); | |
| 121 | } | |
| 122 | ||
| 123 | /** | |
| 124 | * Returns a marker to indicate whether the file has been modified. | |
| 125 | * | |
| 126 | * @return "*" when the file has changed; otherwise null. | |
| 127 | */ | |
| 128 | private Text getModifiedMark() { | |
| 129 | return isModified() ? new Text( "*" ) : null; | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * Called when the user switches tab. | |
| 134 | */ | |
| 135 | private void activated() { | |
| 136 | // Tab is closed or no longer active. | |
| 137 | if( getTabPane() == null || !isSelected() ) { | |
| 138 | return; | |
| 139 | } | |
| 140 | ||
| 141 | // If the tab is devoid of content, load it. | |
| 142 | if( getContent() == null ) { | |
| 143 | readFile(); | |
| 144 | initLayout(); | |
| 145 | initUndoManager(); | |
| 146 | } | |
| 147 | } | |
| 148 | ||
| 149 | private void initLayout() { | |
| 150 | setContent( getScrollPane() ); | |
| 151 | } | |
| 152 | ||
| 153 | /** | |
| 154 | * Tracks undo requests, but can only be called <em>after</em> load. | |
| 155 | */ | |
| 156 | private void initUndoManager() { | |
| 157 | final UndoManager<?> undoManager = getUndoManager(); | |
| 158 | undoManager.forgetHistory(); | |
| 159 | ||
| 160 | // Bind the editor undo manager to the properties. | |
| 161 | mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) ); | |
| 162 | canUndo.bind( undoManager.undoAvailableProperty() ); | |
| 163 | canRedo.bind( undoManager.redoAvailableProperty() ); | |
| 164 | } | |
| 165 | ||
| 166 | private void requestFocus() { | |
| 167 | getEditorPane().requestFocus(); | |
| 168 | } | |
| 169 | ||
| 170 | /** | |
| 171 | * Searches from the caret position forward for the given string. | |
| 172 | * | |
| 173 | * @param needle The text string to match. | |
| 174 | */ | |
| 175 | public void searchNext( final String needle ) { | |
| 176 | final String haystack = getEditorText(); | |
| 177 | int index = haystack.indexOf( needle, getCaretPosition() ); | |
| 178 | ||
| 179 | // Wrap around. | |
| 180 | if( index == -1 ) { | |
| 181 | index = haystack.indexOf( needle ); | |
| 182 | } | |
| 183 | ||
| 184 | if( index >= 0 ) { | |
| 185 | setCaretPosition( index ); | |
| 186 | getEditor().selectRange( index, index + needle.length() ); | |
| 187 | } | |
| 188 | } | |
| 189 | ||
| 190 | /** | |
| 191 | * Gets a reference to the scroll pane that houses the editor. | |
| 192 | * | |
| 193 | * @return The editor's scroll pane, containing a vertical scrollbar. | |
| 194 | */ | |
| 195 | public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { | |
| 196 | return getEditorPane().getScrollPane(); | |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * Returns the index into the text where the caret blinks happily away. | |
| 201 | * | |
| 202 | * @return A number from 0 to the editor's document text length. | |
| 203 | */ | |
| 204 | public int getCaretPosition() { | |
| 205 | return getEditor().getCaretPosition(); | |
| 206 | } | |
| 207 | ||
| 208 | /** | |
| 209 | * Moves the caret to a given offset. | |
| 210 | * | |
| 211 | * @param offset The new caret offset. | |
| 212 | */ | |
| 213 | private void setCaretPosition( final int offset ) { | |
| 214 | getEditor().moveTo( offset ); | |
| 215 | getEditor().requestFollowCaret(); | |
| 216 | } | |
| 217 | ||
| 218 | /** | |
| 219 | * Returns the text area associated with this tab. | |
| 220 | * | |
| 221 | * @return A text editor. | |
| 222 | */ | |
| 223 | private StyleClassedTextArea getEditor() { | |
| 224 | return getEditorPane().getEditor(); | |
| 225 | } | |
| 226 | ||
| 227 | /** | |
| 228 | * Returns true if the given path exactly matches this tab's path. | |
| 229 | * | |
| 230 | * @param check The path to compare against. | |
| 231 | * @return true The paths are the same. | |
| 232 | */ | |
| 233 | public boolean isPath( final Path check ) { | |
| 234 | final Path filePath = getPath(); | |
| 235 | ||
| 236 | return filePath != null && filePath.equals( check ); | |
| 237 | } | |
| 238 | ||
| 239 | /** | |
| 240 | * Reads the entire file contents from the path associated with this tab. | |
| 241 | */ | |
| 242 | private void readFile() { | |
| 243 | final Path path = getPath(); | |
| 244 | final File file = path.toFile(); | |
| 245 | ||
| 246 | try { | |
| 247 | if( file.exists() ) { | |
| 248 | if( file.canWrite() && file.canRead() ) { | |
| 249 | final EditorPane pane = getEditorPane(); | |
| 250 | pane.setText( asString( Files.readAllBytes( path ) ) ); | |
| 251 | pane.scrollToTop(); | |
| 252 | } | |
| 253 | else { | |
| 254 | final String msg = get( "FileEditor.loadFailed.reason.permissions" ); | |
| 255 | alert( "FileEditor.loadFailed.message", file.toString(), msg ); | |
| 256 | } | |
| 257 | } | |
| 258 | } catch( final Exception ex ) { | |
| 259 | alert( ex ); | |
| 260 | } | |
| 261 | } | |
| 262 | ||
| 263 | /** | |
| 264 | * Saves the entire file contents from the path associated with this tab. | |
| 265 | * | |
| 266 | * @return true The file has been saved. | |
| 267 | */ | |
| 268 | public boolean save() { | |
| 269 | try { | |
| 270 | final EditorPane editor = getEditorPane(); | |
| 271 | Files.write( getPath(), asBytes( editor.getText() ) ); | |
| 272 | editor.getUndoManager().mark(); | |
| 273 | return true; | |
| 274 | } catch( final Exception ex ) { | |
| 275 | return popupAlert( | |
| 276 | "FileEditor.saveFailed.title", | |
| 277 | "FileEditor.saveFailed.message", | |
| 278 | ex | |
| 279 | ); | |
| 280 | } | |
| 281 | } | |
| 282 | ||
| 283 | /** | |
| 284 | * Creates an alert dialog and waits for it to close. | |
| 285 | * | |
| 286 | * @param titleKey Resource bundle key for the alert dialog title. | |
| 287 | * @param messageKey Resource bundle key for the alert dialog message. | |
| 288 | * @param e The unexpected happening. | |
| 289 | * @return false | |
| 290 | */ | |
| 291 | @SuppressWarnings("SameParameterValue") | |
| 292 | private boolean popupAlert( | |
| 293 | final String titleKey, final String messageKey, final Exception e ) { | |
| 294 | final Notifier service = getNotifier(); | |
| 295 | final Path filePath = getPath(); | |
| 296 | ||
| 297 | final Notification message = service.createNotification( | |
| 298 | get( titleKey ), | |
| 299 | get( messageKey ), | |
| 300 | filePath == null ? "" : filePath, | |
| 301 | e.getMessage() | |
| 302 | ); | |
| 303 | ||
| 304 | try { | |
| 305 | service.createError( getWindow(), message ).showAndWait(); | |
| 306 | } catch( final Exception ex ) { | |
| 307 | alert( ex ); | |
| 308 | } | |
| 309 | ||
| 310 | return false; | |
| 311 | } | |
| 312 | ||
| 313 | private Window getWindow() { | |
| 314 | final Scene scene = getEditorPane().getScene(); | |
| 315 | ||
| 316 | if( scene == null ) { | |
| 317 | throw new UnsupportedOperationException( "No scene window available" ); | |
| 318 | } | |
| 319 | ||
| 320 | return scene.getWindow(); | |
| 321 | } | |
| 322 | ||
| 323 | /** | |
| 324 | * Returns a best guess at the file encoding. If the encoding could not be | |
| 325 | * detected, this will return the default charset for the JVM. | |
| 326 | * | |
| 327 | * @param bytes The bytes to perform character encoding detection. | |
| 328 | * @return The character encoding. | |
| 329 | */ | |
| 330 | private Charset detectEncoding( final byte[] bytes ) { | |
| 331 | final var detector = new UniversalDetector( null ); | |
| 332 | detector.handleData( bytes, 0, bytes.length ); | |
| 333 | detector.dataEnd(); | |
| 334 | ||
| 335 | final String charset = detector.getDetectedCharset(); | |
| 336 | ||
| 337 | return charset == null | |
| 338 | ? Charset.defaultCharset() | |
| 339 | : Charset.forName( charset.toUpperCase( ENGLISH ) ); | |
| 340 | } | |
| 341 | ||
| 342 | /** | |
| 343 | * Converts the given string to an array of bytes using the encoding that was | |
| 344 | * originally detected (if any) and associated with this file. | |
| 345 | * | |
| 346 | * @param text The text to convert into the original file encoding. | |
| 347 | * @return A series of bytes ready for writing to a file. | |
| 348 | */ | |
| 349 | private byte[] asBytes( final String text ) { | |
| 350 | return text.getBytes( getEncoding() ); | |
| 351 | } | |
| 352 | ||
| 353 | /** | |
| 354 | * Converts the given bytes into a Java String. This will call setEncoding | |
| 355 | * with the encoding detected by the CharsetDetector. | |
| 356 | * | |
| 357 | * @param text The text of unknown character encoding. | |
| 358 | * @return The text, in its auto-detected encoding, as a String. | |
| 359 | */ | |
| 360 | private String asString( final byte[] text ) { | |
| 361 | setEncoding( detectEncoding( text ) ); | |
| 362 | return new String( text, getEncoding() ); | |
| 363 | } | |
| 364 | ||
| 365 | /** | |
| 366 | * Returns the path to the file being edited in this tab. | |
| 367 | * | |
| 368 | * @return A non-null instance. | |
| 369 | */ | |
| 370 | public Path getPath() { | |
| 371 | return mPath; | |
| 372 | } | |
| 373 | ||
| 374 | /** | |
| 375 | * Sets the path to a file for editing and then updates the tab with the | |
| 376 | * file contents. | |
| 377 | * | |
| 378 | * @param path A non-null instance. | |
| 379 | */ | |
| 380 | public void setPath( final Path path ) { | |
| 381 | assert path != null; | |
| 382 | mPath = path; | |
| 383 | ||
| 384 | updateTab(); | |
| 385 | } | |
| 386 | ||
| 387 | public boolean isModified() { | |
| 388 | return mModified.get(); | |
| 389 | } | |
| 390 | ||
| 391 | ReadOnlyBooleanProperty modifiedProperty() { | |
| 392 | return mModified.getReadOnlyProperty(); | |
| 393 | } | |
| 394 | ||
| 395 | BooleanProperty canUndoProperty() { | |
| 396 | return this.canUndo; | |
| 397 | } | |
| 398 | ||
| 399 | BooleanProperty canRedoProperty() { | |
| 400 | return this.canRedo; | |
| 401 | } | |
| 402 | ||
| 403 | private UndoManager<?> getUndoManager() { | |
| 404 | return getEditorPane().getUndoManager(); | |
| 405 | } | |
| 406 | ||
| 407 | /** | |
| 408 | * Forwards to the editor pane's listeners for text change events. | |
| 409 | * | |
| 410 | * @param listener The listener to notify when the text changes. | |
| 411 | */ | |
| 412 | public void addTextChangeListener( final ChangeListener<String> listener ) { | |
| 413 | getEditorPane().addTextChangeListener( listener ); | |
| 414 | } | |
| 415 | ||
| 416 | /** | |
| 417 | * Forwards to the editor pane's listeners for caret change events. | |
| 418 | * | |
| 419 | * @param listener Notified when the caret position changes. | |
| 420 | */ | |
| 421 | public void addCaretPositionListener( | |
| 422 | final ChangeListener<? super Integer> listener ) { | |
| 423 | getEditorPane().addCaretPositionListener( listener ); | |
| 424 | } | |
| 425 | ||
| 426 | /** | |
| 427 | * Forwards to the editor pane's listeners for paragraph index change events. | |
| 428 | * | |
| 429 | * @param listener Notified when the caret's paragraph index changes. | |
| 430 | */ | |
| 431 | public void addCaretParagraphListener( | |
| 432 | final ChangeListener<? super Integer> listener ) { | |
| 433 | getEditorPane().addCaretParagraphListener( listener ); | |
| 434 | } | |
| 435 | ||
| 436 | public <T extends Event> void addEventFilter( | |
| 437 | final EventType<T> eventType, | |
| 438 | final EventHandler<? super T> eventFilter ) { | |
| 439 | getEditor().addEventFilter( eventType, eventFilter ); | |
| 440 | } | |
| 441 | ||
| 442 | /** | |
| 443 | * Forwards the request to the editor pane. | |
| 444 | * | |
| 445 | * @return The text to process. | |
| 446 | */ | |
| 447 | public String getEditorText() { | |
| 448 | return getEditorPane().getText(); | |
| 449 | } | |
| 450 | ||
| 451 | /** | |
| 452 | * Returns the editor pane, or creates one if it doesn't yet exist. | |
| 453 | * | |
| 454 | * @return The editor pane, never null. | |
| 455 | */ | |
| 456 | @NotNull | |
| 457 | public MarkdownEditorPane getEditorPane() { | |
| 458 | return mEditorPane; | |
| 459 | } | |
| 460 | ||
| 461 | /** | |
| 462 | * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been | |
| 463 | * determined. | |
| 464 | * | |
| 465 | * @return The file encoding or UTF-8 if unknown. | |
| 466 | */ | |
| 467 | private Charset getEncoding() { | |
| 468 | return mEncoding; | |
| 469 | } | |
| 470 | ||
| 471 | private void setEncoding( final Charset encoding ) { | |
| 472 | assert encoding != null; | |
| 473 | mEncoding = encoding; | |
| 474 | } | |
| 475 | ||
| 476 | /** | |
| 477 | * Returns the tab title, without any modified indicators. | |
| 478 | * | |
| 479 | * @return The tab title. | |
| 480 | */ | |
| 481 | @Override | |
| 482 | public String toString() { | |
| 483 | return getTabTitle(); | |
| 484 | } | |
| 485 | } | |
| 486 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import com.scrivenvar.service.Options; | |
| 31 | import com.scrivenvar.service.Settings; | |
| 32 | import com.scrivenvar.service.events.Notification; | |
| 33 | import com.scrivenvar.service.events.Notifier; | |
| 34 | import com.scrivenvar.util.Utils; | |
| 35 | import javafx.beans.property.ReadOnlyBooleanProperty; | |
| 36 | import javafx.beans.property.ReadOnlyBooleanWrapper; | |
| 37 | import javafx.beans.property.ReadOnlyObjectProperty; | |
| 38 | import javafx.beans.property.ReadOnlyObjectWrapper; | |
| 39 | import javafx.beans.value.ChangeListener; | |
| 40 | import javafx.collections.ListChangeListener; | |
| 41 | import javafx.collections.ObservableList; | |
| 42 | import javafx.event.Event; | |
| 43 | import javafx.scene.control.Alert; | |
| 44 | import javafx.scene.control.ButtonType; | |
| 45 | import javafx.scene.control.Tab; | |
| 46 | import javafx.scene.control.TabPane; | |
| 47 | import javafx.stage.FileChooser; | |
| 48 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 49 | import javafx.stage.Window; | |
| 50 | ||
| 51 | import java.io.File; | |
| 52 | import java.nio.file.Path; | |
| 53 | import java.util.ArrayList; | |
| 54 | import java.util.List; | |
| 55 | import java.util.Optional; | |
| 56 | import java.util.concurrent.atomic.AtomicReference; | |
| 57 | import java.util.prefs.Preferences; | |
| 58 | import java.util.stream.Collectors; | |
| 59 | ||
| 60 | import static com.scrivenvar.Constants.GLOB_PREFIX_FILE; | |
| 61 | import static com.scrivenvar.Constants.SETTINGS; | |
| 62 | import static com.scrivenvar.FileType.*; | |
| 63 | import static com.scrivenvar.Messages.get; | |
| 64 | import static com.scrivenvar.predicates.PredicateFactory.createFileTypePredicate; | |
| 65 | import static com.scrivenvar.service.events.Notifier.YES; | |
| 66 | ||
| 67 | /** | |
| 68 | * Tab pane for file editors. | |
| 69 | */ | |
| 70 | public final class FileEditorTabPane extends TabPane { | |
| 71 | ||
| 72 | private static final String FILTER_EXTENSION_TITLES = | |
| 73 | "Dialog.file.choose.filter"; | |
| 74 | ||
| 75 | private static final Options sOptions = Services.load( Options.class ); | |
| 76 | private static final Notifier sNotifier = Services.load( Notifier.class ); | |
| 77 | ||
| 78 | private final ReadOnlyObjectWrapper<Path> mOpenDefinition = | |
| 79 | new ReadOnlyObjectWrapper<>(); | |
| 80 | private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor = | |
| 81 | new ReadOnlyObjectWrapper<>(); | |
| 82 | private final ReadOnlyBooleanWrapper mAnyFileEditorModified = | |
| 83 | new ReadOnlyBooleanWrapper(); | |
| 84 | private final ChangeListener<Integer> mCaretPositionListener; | |
| 85 | private final ChangeListener<Integer> mCaretParagraphListener; | |
| 86 | ||
| 87 | /** | |
| 88 | * Constructs a new file editor tab pane. | |
| 89 | * | |
| 90 | * @param caretPositionListener Listens for changes to caret position so | |
| 91 | * that the status bar can update. | |
| 92 | * @param caretParagraphListener Listens for changes to the caret's paragraph | |
| 93 | * so that scrolling may occur. | |
| 94 | */ | |
| 95 | public FileEditorTabPane( | |
| 96 | final ChangeListener<Integer> caretPositionListener, | |
| 97 | final ChangeListener<Integer> caretParagraphListener ) { | |
| 98 | final ObservableList<Tab> tabs = getTabs(); | |
| 99 | ||
| 100 | setFocusTraversable( false ); | |
| 101 | setTabClosingPolicy( TabClosingPolicy.ALL_TABS ); | |
| 102 | ||
| 103 | addTabSelectionListener( | |
| 104 | ( tabPane, oldTab, newTab ) -> { | |
| 105 | if( newTab != null ) { | |
| 106 | mActiveFileEditor.set( (FileEditorTab) newTab ); | |
| 107 | } | |
| 108 | } | |
| 109 | ); | |
| 110 | ||
| 111 | final ChangeListener<Boolean> modifiedListener = | |
| 112 | ( observable, oldValue, newValue ) -> { | |
| 113 | for( final Tab tab : tabs ) { | |
| 114 | if( ((FileEditorTab) tab).isModified() ) { | |
| 115 | mAnyFileEditorModified.set( true ); | |
| 116 | break; | |
| 117 | } | |
| 118 | } | |
| 119 | }; | |
| 120 | ||
| 121 | tabs.addListener( | |
| 122 | (ListChangeListener<Tab>) change -> { | |
| 123 | while( change.next() ) { | |
| 124 | if( change.wasAdded() ) { | |
| 125 | change.getAddedSubList().forEach( | |
| 126 | ( tab ) -> { | |
| 127 | final var fet = (FileEditorTab) tab; | |
| 128 | fet.modifiedProperty().addListener( modifiedListener ); | |
| 129 | } ); | |
| 130 | } | |
| 131 | else if( change.wasRemoved() ) { | |
| 132 | change.getRemoved().forEach( | |
| 133 | ( tab ) -> { | |
| 134 | final var fet = (FileEditorTab) tab; | |
| 135 | fet.modifiedProperty().removeListener( modifiedListener ); | |
| 136 | } | |
| 137 | ); | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | // Changes in the tabs may also change anyFileEditorModified property | |
| 142 | // (e.g. closed modified file) | |
| 143 | modifiedListener.changed( null, null, null ); | |
| 144 | } | |
| 145 | ); | |
| 146 | ||
| 147 | mCaretPositionListener = caretPositionListener; | |
| 148 | mCaretParagraphListener = caretParagraphListener; | |
| 149 | } | |
| 150 | ||
| 151 | /** | |
| 152 | * Allows observers to be notified when the current file editor tab changes. | |
| 153 | * | |
| 154 | * @param listener The listener to notify of tab change events. | |
| 155 | */ | |
| 156 | public void addTabSelectionListener( final ChangeListener<Tab> listener ) { | |
| 157 | // Observe the tab so that when a new tab is opened or selected, | |
| 158 | // a notification is kicked off. | |
| 159 | getSelectionModel().selectedItemProperty().addListener( listener ); | |
| 160 | } | |
| 161 | ||
| 162 | /** | |
| 163 | * Returns the tab that has keyboard focus. | |
| 164 | * | |
| 165 | * @return A non-null instance. | |
| 166 | */ | |
| 167 | public FileEditorTab getActiveFileEditor() { | |
| 168 | return mActiveFileEditor.get(); | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Returns the property corresponding to the tab that has focus. | |
| 173 | * | |
| 174 | * @return A non-null instance. | |
| 175 | */ | |
| 176 | public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() { | |
| 177 | return mActiveFileEditor.getReadOnlyProperty(); | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Property that can answer whether the text has been modified. | |
| 182 | * | |
| 183 | * @return A non-null instance, true meaning the content has not been saved. | |
| 184 | */ | |
| 185 | ReadOnlyBooleanProperty anyFileEditorModifiedProperty() { | |
| 186 | return mAnyFileEditorModified.getReadOnlyProperty(); | |
| 187 | } | |
| 188 | ||
| 189 | /** | |
| 190 | * Creates a new editor instance from the given path. | |
| 191 | * | |
| 192 | * @param path The file to open. | |
| 193 | * @return A non-null instance. | |
| 194 | */ | |
| 195 | private FileEditorTab createFileEditor( final Path path ) { | |
| 196 | assert path != null; | |
| 197 | ||
| 198 | final FileEditorTab tab = new FileEditorTab( path ); | |
| 199 | ||
| 200 | tab.setOnCloseRequest( e -> { | |
| 201 | if( !canCloseEditor( tab ) ) { | |
| 202 | e.consume(); | |
| 203 | } | |
| 204 | else if( isActiveFileEditor( tab ) ) { | |
| 205 | // Prevent prompting the user to save when there are no file editor | |
| 206 | // tabs open. | |
| 207 | mActiveFileEditor.set( null ); | |
| 208 | } | |
| 209 | } ); | |
| 210 | ||
| 211 | tab.addCaretPositionListener( mCaretPositionListener ); | |
| 212 | tab.addCaretParagraphListener( mCaretParagraphListener ); | |
| 213 | ||
| 214 | return tab; | |
| 215 | } | |
| 216 | ||
| 217 | private boolean isActiveFileEditor( final FileEditorTab tab ) { | |
| 218 | return getActiveFileEditor() == tab; | |
| 219 | } | |
| 220 | ||
| 221 | private Path getDefaultPath() { | |
| 222 | final String filename = getDefaultFilename(); | |
| 223 | return (new File( filename )).toPath(); | |
| 224 | } | |
| 225 | ||
| 226 | private String getDefaultFilename() { | |
| 227 | return getSettings().getSetting( "file.default", "untitled.md" ); | |
| 228 | } | |
| 229 | ||
| 230 | /** | |
| 231 | * Called to add a new {@link FileEditorTab} to the tab pane. | |
| 232 | */ | |
| 233 | void newEditor() { | |
| 234 | final FileEditorTab tab = createFileEditor( getDefaultPath() ); | |
| 235 | ||
| 236 | getTabs().add( tab ); | |
| 237 | getSelectionModel().select( tab ); | |
| 238 | } | |
| 239 | ||
| 240 | void openFileDialog() { | |
| 241 | final String title = get( "Dialog.file.choose.open.title" ); | |
| 242 | final FileChooser dialog = createFileChooser( title ); | |
| 243 | final List<File> files = dialog.showOpenMultipleDialog( getWindow() ); | |
| 244 | ||
| 245 | if( files != null ) { | |
| 246 | openFiles( files ); | |
| 247 | } | |
| 248 | } | |
| 249 | ||
| 250 | /** | |
| 251 | * Opens the files into new editors, unless one of those files was a | |
| 252 | * definition file. The definition file is loaded into the definition pane, | |
| 253 | * but only the first one selected (multiple definition files will result in a | |
| 254 | * warning). | |
| 255 | * | |
| 256 | * @param files The list of non-definition files that the were requested to | |
| 257 | * open. | |
| 258 | */ | |
| 259 | private void openFiles( final List<File> files ) { | |
| 260 | final List<String> extensions = | |
| 261 | createExtensionFilter( DEFINITION ).getExtensions(); | |
| 262 | final var predicate = createFileTypePredicate( extensions ); | |
| 263 | ||
| 264 | // The user might have opened multiple definitions files. These will | |
| 265 | // be discarded from the text editable files. | |
| 266 | final var definitions | |
| 267 | = files.stream().filter( predicate ).collect( Collectors.toList() ); | |
| 268 | ||
| 269 | // Create a modifiable list to remove any definition files that were | |
| 270 | // opened. | |
| 271 | final var editors = new ArrayList<>( files ); | |
| 272 | ||
| 273 | if( !editors.isEmpty() ) { | |
| 274 | saveLastDirectory( editors.get( 0 ) ); | |
| 275 | } | |
| 276 | ||
| 277 | editors.removeAll( definitions ); | |
| 278 | ||
| 279 | // Open editor-friendly files (e.g,. Markdown, XML) in new tabs. | |
| 280 | if( !editors.isEmpty() ) { | |
| 281 | openEditors( editors, 0 ); | |
| 282 | } | |
| 283 | ||
| 284 | if( !definitions.isEmpty() ) { | |
| 285 | openDefinition( definitions.get( 0 ) ); | |
| 286 | } | |
| 287 | } | |
| 288 | ||
| 289 | private void openEditors( final List<File> files, final int activeIndex ) { | |
| 290 | final int fileTally = files.size(); | |
| 291 | final List<Tab> tabs = getTabs(); | |
| 292 | ||
| 293 | // Close single unmodified "Untitled" tab. | |
| 294 | if( tabs.size() == 1 ) { | |
| 295 | final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 )); | |
| 296 | ||
| 297 | if( fileEditor.getPath() == null && !fileEditor.isModified() ) { | |
| 298 | closeEditor( fileEditor, false ); | |
| 299 | } | |
| 300 | } | |
| 301 | ||
| 302 | for( int i = 0; i < fileTally; i++ ) { | |
| 303 | final Path path = files.get( i ).toPath(); | |
| 304 | ||
| 305 | FileEditorTab fileEditorTab = findEditor( path ); | |
| 306 | ||
| 307 | // Only open new files. | |
| 308 | if( fileEditorTab == null ) { | |
| 309 | fileEditorTab = createFileEditor( path ); | |
| 310 | getTabs().add( fileEditorTab ); | |
| 311 | } | |
| 312 | ||
| 313 | // Select the first file in the list. | |
| 314 | if( i == activeIndex ) { | |
| 315 | getSelectionModel().select( fileEditorTab ); | |
| 316 | } | |
| 317 | } | |
| 318 | } | |
| 319 | ||
| 320 | /** | |
| 321 | * Returns a property that changes when a new definition file is opened. | |
| 322 | * | |
| 323 | * @return The path to a definition file that was opened. | |
| 324 | */ | |
| 325 | public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() { | |
| 326 | return getOnOpenDefinitionFile().getReadOnlyProperty(); | |
| 327 | } | |
| 328 | ||
| 329 | private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() { | |
| 330 | return mOpenDefinition; | |
| 331 | } | |
| 332 | ||
| 333 | /** | |
| 334 | * Called when the user has opened a definition file (using the file open | |
| 335 | * dialog box). This will replace the current set of definitions for the | |
| 336 | * active tab. | |
| 337 | * | |
| 338 | * @param definition The file to open. | |
| 339 | */ | |
| 340 | private void openDefinition( final File definition ) { | |
| 341 | // TODO: Prevent reading this file twice when a new text document is opened. | |
| 342 | // (might be a matter of checking the value first). | |
| 343 | getOnOpenDefinitionFile().set( definition.toPath() ); | |
| 344 | } | |
| 345 | ||
| 346 | /** | |
| 347 | * Called when the contents of the editor are to be saved. | |
| 348 | * | |
| 349 | * @param tab The tab containing content to save. | |
| 350 | * @return true The contents were saved (or needn't be saved). | |
| 351 | */ | |
| 352 | public boolean saveEditor( final FileEditorTab tab ) { | |
| 353 | if( tab == null || !tab.isModified() ) { | |
| 354 | return true; | |
| 355 | } | |
| 356 | ||
| 357 | return tab.getPath() == null ? saveEditorAs( tab ) : tab.save(); | |
| 358 | } | |
| 359 | ||
| 360 | /** | |
| 361 | * Opens the Save As dialog for the user to save the content under a new | |
| 362 | * path. | |
| 363 | * | |
| 364 | * @param tab The tab with contents to save. | |
| 365 | * @return true The contents were saved, or the tab was null. | |
| 366 | */ | |
| 367 | public boolean saveEditorAs( final FileEditorTab tab ) { | |
| 368 | if( tab == null ) { | |
| 369 | return true; | |
| 370 | } | |
| 371 | ||
| 372 | getSelectionModel().select( tab ); | |
| 373 | ||
| 374 | final FileChooser fileChooser = createFileChooser( get( | |
| 375 | "Dialog.file.choose.save.title" ) ); | |
| 376 | final File file = fileChooser.showSaveDialog( getWindow() ); | |
| 377 | if( file == null ) { | |
| 378 | return false; | |
| 379 | } | |
| 380 | ||
| 381 | saveLastDirectory( file ); | |
| 382 | tab.setPath( file.toPath() ); | |
| 383 | ||
| 384 | return tab.save(); | |
| 385 | } | |
| 386 | ||
| 387 | void saveAllEditors() { | |
| 388 | for( final FileEditorTab fileEditor : getAllEditors() ) { | |
| 389 | saveEditor( fileEditor ); | |
| 390 | } | |
| 391 | } | |
| 392 | ||
| 393 | /** | |
| 394 | * Answers whether the file has had modifications. ' | |
| 395 | * | |
| 396 | * @param tab THe tab to check for modifications. | |
| 397 | * @return false The file is unmodified. | |
| 398 | */ | |
| 399 | @SuppressWarnings("BooleanMethodIsAlwaysInverted") | |
| 400 | boolean canCloseEditor( final FileEditorTab tab ) { | |
| 401 | final AtomicReference<Boolean> canClose = new AtomicReference<>(); | |
| 402 | canClose.set( true ); | |
| 403 | ||
| 404 | if( tab.isModified() ) { | |
| 405 | final Notification message = getNotifyService().createNotification( | |
| 406 | Messages.get( "Alert.file.close.title" ), | |
| 407 | Messages.get( "Alert.file.close.text" ), | |
| 408 | tab.getText() | |
| 409 | ); | |
| 410 | ||
| 411 | final Alert confirmSave = getNotifyService().createConfirmation( | |
| 412 | getWindow(), message ); | |
| 413 | ||
| 414 | final Optional<ButtonType> buttonType = confirmSave.showAndWait(); | |
| 415 | ||
| 416 | buttonType.ifPresent( | |
| 417 | save -> canClose.set( | |
| 418 | save == YES ? saveEditor( tab ) : save == ButtonType.NO | |
| 419 | ) | |
| 420 | ); | |
| 421 | } | |
| 422 | ||
| 423 | return canClose.get(); | |
| 424 | } | |
| 425 | ||
| 426 | boolean closeEditor( final FileEditorTab tab, final boolean save ) { | |
| 427 | if( tab == null ) { | |
| 428 | return true; | |
| 429 | } | |
| 430 | ||
| 431 | if( save ) { | |
| 432 | Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT ); | |
| 433 | Event.fireEvent( tab, event ); | |
| 434 | ||
| 435 | if( event.isConsumed() ) { | |
| 436 | return false; | |
| 437 | } | |
| 438 | } | |
| 439 | ||
| 440 | getTabs().remove( tab ); | |
| 441 | ||
| 442 | if( tab.getOnClosed() != null ) { | |
| 443 | Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) ); | |
| 444 | } | |
| 445 | ||
| 446 | return true; | |
| 447 | } | |
| 448 | ||
| 449 | boolean closeAllEditors() { | |
| 450 | final FileEditorTab[] allEditors = getAllEditors(); | |
| 451 | final FileEditorTab activeEditor = getActiveFileEditor(); | |
| 452 | ||
| 453 | // try to save active tab first because in case the user decides to cancel, | |
| 454 | // then it stays active | |
| 455 | if( activeEditor != null && !canCloseEditor( activeEditor ) ) { | |
| 456 | return false; | |
| 457 | } | |
| 458 | ||
| 459 | // This should be called any time a tab changes. | |
| 460 | persistPreferences(); | |
| 461 | ||
| 462 | // save modified tabs | |
| 463 | for( int i = 0; i < allEditors.length; i++ ) { | |
| 464 | final FileEditorTab fileEditor = allEditors[ i ]; | |
| 465 | ||
| 466 | if( fileEditor == activeEditor ) { | |
| 467 | continue; | |
| 468 | } | |
| 469 | ||
| 470 | if( fileEditor.isModified() ) { | |
| 471 | // activate the modified tab to make its modified content visible to | |
| 472 | // the user | |
| 473 | getSelectionModel().select( i ); | |
| 474 | ||
| 475 | if( !canCloseEditor( fileEditor ) ) { | |
| 476 | return false; | |
| 477 | } | |
| 478 | } | |
| 479 | } | |
| 480 | ||
| 481 | // Close all tabs. | |
| 482 | for( final FileEditorTab fileEditor : allEditors ) { | |
| 483 | if( !closeEditor( fileEditor, false ) ) { | |
| 484 | return false; | |
| 485 | } | |
| 486 | } | |
| 487 | ||
| 488 | return getTabs().isEmpty(); | |
| 489 | } | |
| 490 | ||
| 491 | private FileEditorTab[] getAllEditors() { | |
| 492 | final ObservableList<Tab> tabs = getTabs(); | |
| 493 | final int length = tabs.size(); | |
| 494 | final FileEditorTab[] allEditors = new FileEditorTab[ length ]; | |
| 495 | ||
| 496 | for( int i = 0; i < length; i++ ) { | |
| 497 | allEditors[ i ] = (FileEditorTab) tabs.get( i ); | |
| 498 | } | |
| 499 | ||
| 500 | return allEditors; | |
| 501 | } | |
| 502 | ||
| 503 | /** | |
| 504 | * Returns the file editor tab that has the given path. | |
| 505 | * | |
| 506 | * @return null No file editor tab for the given path was found. | |
| 507 | */ | |
| 508 | private FileEditorTab findEditor( final Path path ) { | |
| 509 | for( final Tab tab : getTabs() ) { | |
| 510 | final FileEditorTab fileEditor = (FileEditorTab) tab; | |
| 511 | ||
| 512 | if( fileEditor.isPath( path ) ) { | |
| 513 | return fileEditor; | |
| 514 | } | |
| 515 | } | |
| 516 | ||
| 517 | return null; | |
| 518 | } | |
| 519 | ||
| 520 | private FileChooser createFileChooser( String title ) { | |
| 521 | final FileChooser fileChooser = new FileChooser(); | |
| 522 | ||
| 523 | fileChooser.setTitle( title ); | |
| 524 | fileChooser.getExtensionFilters().addAll( | |
| 525 | createExtensionFilters() ); | |
| 526 | ||
| 527 | final String lastDirectory = getPreferences().get( "lastDirectory", null ); | |
| 528 | File file = new File( (lastDirectory != null) ? lastDirectory : "." ); | |
| 529 | ||
| 530 | if( !file.isDirectory() ) { | |
| 531 | file = new File( "." ); | |
| 532 | } | |
| 533 | ||
| 534 | fileChooser.setInitialDirectory( file ); | |
| 535 | return fileChooser; | |
| 536 | } | |
| 537 | ||
| 538 | private List<ExtensionFilter> createExtensionFilters() { | |
| 539 | final List<ExtensionFilter> list = new ArrayList<>(); | |
| 540 | ||
| 541 | // TODO: Return a list of all properties that match the filter prefix. | |
| 542 | // This will allow dynamic filters to be added and removed just by | |
| 543 | // updating the properties file. | |
| 544 | list.add( createExtensionFilter( ALL ) ); | |
| 545 | list.add( createExtensionFilter( SOURCE ) ); | |
| 546 | list.add( createExtensionFilter( DEFINITION ) ); | |
| 547 | list.add( createExtensionFilter( XML ) ); | |
| 548 | return list; | |
| 549 | } | |
| 550 | ||
| 551 | /** | |
| 552 | * Returns a filter for file name extensions recognized by the application | |
| 553 | * that can be opened by the user. | |
| 554 | * | |
| 555 | * @param filetype Used to find the globbing pattern for extensions. | |
| 556 | * @return A filename filter suitable for use by a FileDialog instance. | |
| 557 | */ | |
| 558 | private ExtensionFilter createExtensionFilter( final FileType filetype ) { | |
| 559 | final String tKey = String.format( "%s.title.%s", | |
| 560 | FILTER_EXTENSION_TITLES, | |
| 561 | filetype ); | |
| 562 | final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype ); | |
| 563 | ||
| 564 | return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) ); | |
| 565 | } | |
| 566 | ||
| 567 | private void saveLastDirectory( final File file ) { | |
| 568 | getPreferences().put( "lastDirectory", file.getParent() ); | |
| 569 | } | |
| 570 | ||
| 571 | public void initPreferences() { | |
| 572 | int activeIndex = 0; | |
| 573 | ||
| 574 | final Preferences preferences = getPreferences(); | |
| 575 | final String[] fileNames = Utils.getPrefsStrings( preferences, "file" ); | |
| 576 | final String activeFileName = preferences.get( "activeFile", null ); | |
| 577 | ||
| 578 | final List<File> files = new ArrayList<>( fileNames.length ); | |
| 579 | ||
| 580 | for( final String fileName : fileNames ) { | |
| 581 | final File file = new File( fileName ); | |
| 582 | ||
| 583 | if( file.exists() ) { | |
| 584 | files.add( file ); | |
| 585 | ||
| 586 | if( fileName.equals( activeFileName ) ) { | |
| 587 | activeIndex = files.size() - 1; | |
| 588 | } | |
| 589 | } | |
| 590 | } | |
| 591 | ||
| 592 | if( files.isEmpty() ) { | |
| 593 | newEditor(); | |
| 594 | } | |
| 595 | else { | |
| 596 | openEditors( files, activeIndex ); | |
| 597 | } | |
| 598 | } | |
| 599 | ||
| 600 | public void persistPreferences() { | |
| 601 | final var allEditors = getTabs(); | |
| 602 | final List<String> fileNames = new ArrayList<>( allEditors.size() ); | |
| 603 | ||
| 604 | for( final var tab : allEditors ) { | |
| 605 | final var fileEditor = (FileEditorTab) tab; | |
| 606 | final var filePath = fileEditor.getPath(); | |
| 607 | ||
| 608 | if( filePath != null ) { | |
| 609 | fileNames.add( filePath.toString() ); | |
| 610 | } | |
| 611 | } | |
| 612 | ||
| 613 | final var preferences = getPreferences(); | |
| 614 | Utils.putPrefsStrings( preferences, | |
| 615 | "file", | |
| 616 | fileNames.toArray( new String[ 0 ] ) ); | |
| 617 | ||
| 618 | final var activeEditor = getActiveFileEditor(); | |
| 619 | final var filePath = activeEditor == null ? null : activeEditor.getPath(); | |
| 620 | ||
| 621 | if( filePath == null ) { | |
| 622 | preferences.remove( "activeFile" ); | |
| 623 | } | |
| 624 | else { | |
| 625 | preferences.put( "activeFile", filePath.toString() ); | |
| 626 | } | |
| 627 | } | |
| 628 | ||
| 629 | private List<String> getExtensions( final String key ) { | |
| 630 | return getSettings().getStringSettingList( key ); | |
| 631 | } | |
| 632 | ||
| 633 | private Notifier getNotifyService() { | |
| 634 | return sNotifier; | |
| 635 | } | |
| 636 | ||
| 637 | private Settings getSettings() { | |
| 638 | return SETTINGS; | |
| 639 | } | |
| 640 | ||
| 641 | protected Options getOptions() { | |
| 642 | return sOptions; | |
| 643 | } | |
| 644 | ||
| 645 | private Window getWindow() { | |
| 646 | return getScene().getWindow(); | |
| 647 | } | |
| 648 | ||
| 649 | private Preferences getPreferences() { | |
| 650 | return getOptions().getState(); | |
| 651 | } | |
| 652 | } | |
| 653 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents different file type classifications. These are high-level mappings | |
| 32 | * that correspond to the list of glob patterns found within {@code | |
| 33 | * settings.properties}. | |
| 34 | */ | |
| 35 | public enum FileType { | |
| 36 | ||
| 37 | ALL( "all" ), | |
| 38 | RMARKDOWN( "rmarkdown" ), | |
| 39 | RXML( "rxml" ), | |
| 40 | SOURCE( "source" ), | |
| 41 | DEFINITION( "definition" ), | |
| 42 | XML( "xml" ), | |
| 43 | CSV( "csv" ), | |
| 44 | JSON( "json" ), | |
| 45 | TOML( "toml" ), | |
| 46 | YAML( "yaml" ), | |
| 47 | PROPERTIES( "properties" ), | |
| 48 | UNKNOWN( "unknown" ); | |
| 49 | ||
| 50 | private final String mType; | |
| 51 | ||
| 52 | /** | |
| 53 | * Default constructor for enumerated file type. | |
| 54 | * | |
| 55 | * @param type Human-readable name for the file type. | |
| 56 | */ | |
| 57 | FileType( final String type ) { | |
| 58 | mType = type; | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Returns the file type that corresponds to the given string. | |
| 63 | * | |
| 64 | * @param type The string to compare against this enumeration of file types. | |
| 65 | * @return The corresponding File Type for the given string. | |
| 66 | * @throws IllegalArgumentException Type not found. | |
| 67 | */ | |
| 68 | public static FileType from( final String type ) { | |
| 69 | for( final FileType fileType : FileType.values() ) { | |
| 70 | if( fileType.isType( type ) ) { | |
| 71 | return fileType; | |
| 72 | } | |
| 73 | } | |
| 74 | ||
| 75 | throw new IllegalArgumentException( type ); | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * Answers whether this file type matches the given string, case insensitive | |
| 80 | * comparison. | |
| 81 | * | |
| 82 | * @param type Presumably a file name extension to check against. | |
| 83 | * @return true The given extension corresponds to this enumerated type. | |
| 84 | */ | |
| 85 | public boolean isType( final String type ) { | |
| 86 | return getType().equalsIgnoreCase( type ); | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * Returns the human-readable name for the file type. | |
| 91 | * | |
| 92 | * @return A non-null instance. | |
| 93 | */ | |
| 94 | private String getType() { | |
| 95 | return mType; | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Returns the lowercase version of the file name extension. | |
| 100 | * | |
| 101 | * @return The file name, in lower case. | |
| 102 | */ | |
| 103 | @Override | |
| 104 | public String toString() { | |
| 105 | return getType(); | |
| 106 | } | |
| 107 | } | |
| 108 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import java.io.IOException; | |
| 31 | import java.io.InputStream; | |
| 32 | import java.util.Calendar; | |
| 33 | import java.util.Properties; | |
| 34 | ||
| 35 | import static java.lang.String.format; | |
| 36 | ||
| 37 | /** | |
| 38 | * Launches the application using the {@link Main} class. | |
| 39 | * | |
| 40 | * <p> | |
| 41 | * This is required until modules are implemented, which may never happen | |
| 42 | * because the application should be ported away from Java and JavaFX. | |
| 43 | * </p> | |
| 44 | */ | |
| 45 | public class Launcher { | |
| 46 | /** | |
| 47 | * Delegates to the application entry point. | |
| 48 | * | |
| 49 | * @param args Command-line arguments. | |
| 50 | */ | |
| 51 | public static void main( final String[] args ) throws IOException { | |
| 52 | showAppInfo(); | |
| 53 | Main.main( args ); | |
| 54 | } | |
| 55 | ||
| 56 | @SuppressWarnings("RedundantStringFormatCall") | |
| 57 | private static void showAppInfo() throws IOException { | |
| 58 | out( format( "%s version %s", getTitle(), getVersion() ) ); | |
| 59 | out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) ); | |
| 60 | out( format( "Portions copyright 2020 Karl Tauber." ) ); | |
| 61 | } | |
| 62 | ||
| 63 | private static void out( final String s ) { | |
| 64 | System.out.println( s ); | |
| 65 | } | |
| 66 | ||
| 67 | private static String getTitle() throws IOException { | |
| 68 | final Properties properties = loadProperties( "messages.properties" ); | |
| 69 | return properties.getProperty( "Main.title" ); | |
| 70 | } | |
| 71 | ||
| 72 | private static String getVersion() throws IOException { | |
| 73 | final Properties properties = loadProperties( "app.properties" ); | |
| 74 | return properties.getProperty( "application.version" ); | |
| 75 | } | |
| 76 | ||
| 77 | private static String getYear() { | |
| 78 | return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) ); | |
| 79 | } | |
| 80 | ||
| 81 | @SuppressWarnings("SameParameterValue") | |
| 82 | private static Properties loadProperties( final String resource ) | |
| 83 | throws IOException { | |
| 84 | final Properties properties = new Properties(); | |
| 85 | properties.load( getResourceAsStream( getResourceName( resource ) ) ); | |
| 86 | return properties; | |
| 87 | } | |
| 88 | ||
| 89 | private static String getResourceName( final String resource ) { | |
| 90 | return format( "%s/%s", getPackagePath(), resource ); | |
| 91 | } | |
| 92 | ||
| 93 | private static String getPackagePath() { | |
| 94 | return Launcher.class.getPackageName().replace( '.', '/' ); | |
| 95 | } | |
| 96 | ||
| 97 | private static InputStream getResourceAsStream( final String resource ) { | |
| 98 | return Launcher.class.getClassLoader().getResourceAsStream( resource ); | |
| 99 | } | |
| 100 | } | |
| 101 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import com.scrivenvar.preferences.FilePreferencesFactory; | |
| 31 | import com.scrivenvar.service.Options; | |
| 32 | import com.scrivenvar.service.Snitch; | |
| 33 | import com.scrivenvar.util.ResourceWalker; | |
| 34 | import com.scrivenvar.util.StageState; | |
| 35 | import javafx.application.Application; | |
| 36 | import javafx.scene.Scene; | |
| 37 | import javafx.scene.image.Image; | |
| 38 | import javafx.stage.Stage; | |
| 39 | ||
| 40 | import java.awt.*; | |
| 41 | import java.io.FileInputStream; | |
| 42 | import java.io.IOException; | |
| 43 | import java.io.InputStream; | |
| 44 | import java.net.URI; | |
| 45 | import java.util.Map; | |
| 46 | import java.util.logging.LogManager; | |
| 47 | ||
| 48 | import static com.scrivenvar.Constants.*; | |
| 49 | import static com.scrivenvar.Messages.get; | |
| 50 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 51 | import static java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment; | |
| 52 | import static java.awt.font.TextAttribute.*; | |
| 53 | import static javafx.scene.input.KeyCode.F11; | |
| 54 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 55 | ||
| 56 | /** | |
| 57 | * Application entry point. The application allows users to edit Markdown | |
| 58 | * files and see a real-time preview of the edits. | |
| 59 | */ | |
| 60 | public final class Main extends Application { | |
| 61 | ||
| 62 | static { | |
| 63 | // Suppress logging to standard output. | |
| 64 | LogManager.getLogManager().reset(); | |
| 65 | ||
| 66 | // Suppress logging to standard error. | |
| 67 | System.err.close(); | |
| 68 | } | |
| 69 | ||
| 70 | private final Options mOptions = Services.load( Options.class ); | |
| 71 | private final Snitch mSnitch = Services.load( Snitch.class ); | |
| 72 | ||
| 73 | private final Thread mSnitchThread = new Thread( getSnitch() ); | |
| 74 | private final MainWindow mMainWindow = new MainWindow(); | |
| 75 | ||
| 76 | @SuppressWarnings({"FieldCanBeLocal", "unused"}) | |
| 77 | private StageState mStageState; | |
| 78 | ||
| 79 | /** | |
| 80 | * Application entry point. | |
| 81 | * | |
| 82 | * @param args Command-line arguments. | |
| 83 | */ | |
| 84 | public static void main( final String[] args ) { | |
| 85 | initPreferences(); | |
| 86 | initFonts(); | |
| 87 | launch( args ); | |
| 88 | } | |
| 89 | ||
| 90 | /** | |
| 91 | * JavaFX entry point. | |
| 92 | * | |
| 93 | * @param stage The primary application stage. | |
| 94 | */ | |
| 95 | @Override | |
| 96 | public void start( final Stage stage ) { | |
| 97 | initState( stage ); | |
| 98 | initStage( stage ); | |
| 99 | initSnitch(); | |
| 100 | ||
| 101 | stage.show(); | |
| 102 | ||
| 103 | // After the stage is visible, the panel dimensions are | |
| 104 | // known, which allows scaling images to fit the preview panel. | |
| 105 | getMainWindow().init(); | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * This needs to run before the windowing system kicks in, otherwise the | |
| 110 | * fonts will not be found. | |
| 111 | */ | |
| 112 | @SuppressWarnings({"rawtypes", "unchecked"}) | |
| 113 | public static void initFonts() { | |
| 114 | final var ge = getLocalGraphicsEnvironment(); | |
| 115 | ||
| 116 | try { | |
| 117 | ResourceWalker.walk( | |
| 118 | FONT_DIRECTORY, path -> { | |
| 119 | final var uri = path.toUri(); | |
| 120 | final var filename = path.toString(); | |
| 121 | ||
| 122 | try( final var is = openFont( uri, filename ) ) { | |
| 123 | final var font = Font.createFont( Font.TRUETYPE_FONT, is ); | |
| 124 | final Map attributes = font.getAttributes(); | |
| 125 | ||
| 126 | attributes.put( LIGATURES, LIGATURES_ON ); | |
| 127 | attributes.put( KERNING, KERNING_ON ); | |
| 128 | ge.registerFont( font.deriveFont( attributes ) ); | |
| 129 | } catch( final Exception e ) { | |
| 130 | alert( e ); | |
| 131 | } | |
| 132 | } | |
| 133 | ); | |
| 134 | } catch( final Exception e ) { | |
| 135 | alert( e ); | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | private static InputStream openFont( final URI uri, final String filename ) | |
| 140 | throws IOException { | |
| 141 | return uri.getScheme().equals( "jar" ) | |
| 142 | ? Main.class.getResourceAsStream( filename ) | |
| 143 | : new FileInputStream( filename ); | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Sets the factory used for reading user preferences. | |
| 148 | */ | |
| 149 | private static void initPreferences() { | |
| 150 | System.setProperty( | |
| 151 | "java.util.prefs.PreferencesFactory", | |
| 152 | FilePreferencesFactory.class.getName() | |
| 153 | ); | |
| 154 | } | |
| 155 | ||
| 156 | private void initState( final Stage stage ) { | |
| 157 | mStageState = new StageState( stage, getOptions().getState() ); | |
| 158 | } | |
| 159 | ||
| 160 | private void initStage( final Stage stage ) { | |
| 161 | stage.getIcons().addAll( | |
| 162 | createImage( FILE_LOGO_16 ), | |
| 163 | createImage( FILE_LOGO_32 ), | |
| 164 | createImage( FILE_LOGO_128 ), | |
| 165 | createImage( FILE_LOGO_256 ), | |
| 166 | createImage( FILE_LOGO_512 ) ); | |
| 167 | stage.setTitle( getApplicationTitle() ); | |
| 168 | stage.setScene( getScene() ); | |
| 169 | ||
| 170 | stage.addEventHandler( KEY_PRESSED, event -> { | |
| 171 | if( F11.equals( event.getCode() ) ) { | |
| 172 | stage.setFullScreen( !stage.isFullScreen() ); | |
| 173 | } | |
| 174 | } ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Watch for file system changes. | |
| 179 | */ | |
| 180 | private void initSnitch() { | |
| 181 | getSnitchThread().start(); | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Stops the snitch service, if its running. | |
| 186 | * | |
| 187 | * @throws InterruptedException Couldn't stop the snitch thread. | |
| 188 | */ | |
| 189 | @Override | |
| 190 | public void stop() throws InterruptedException { | |
| 191 | getSnitch().stop(); | |
| 192 | ||
| 193 | final Thread thread = getSnitchThread(); | |
| 194 | thread.interrupt(); | |
| 195 | thread.join(); | |
| 196 | } | |
| 197 | ||
| 198 | private Snitch getSnitch() { | |
| 199 | return mSnitch; | |
| 200 | } | |
| 201 | ||
| 202 | private Thread getSnitchThread() { | |
| 203 | return mSnitchThread; | |
| 204 | } | |
| 205 | ||
| 206 | private Options getOptions() { | |
| 207 | return mOptions; | |
| 208 | } | |
| 209 | ||
| 210 | private MainWindow getMainWindow() { | |
| 211 | return mMainWindow; | |
| 212 | } | |
| 213 | ||
| 214 | private Scene getScene() { | |
| 215 | return getMainWindow().getScene(); | |
| 216 | } | |
| 217 | ||
| 218 | private String getApplicationTitle() { | |
| 219 | return get( "Main.title" ); | |
| 220 | } | |
| 221 | ||
| 222 | private Image createImage( final String filename ) { | |
| 223 | return new Image( filename ); | |
| 224 | } | |
| 225 | } | |
| 226 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import com.dlsc.preferencesfx.PreferencesFxEvent; | |
| 31 | import com.scrivenvar.definition.DefinitionFactory; | |
| 32 | import com.scrivenvar.definition.DefinitionPane; | |
| 33 | import com.scrivenvar.definition.DefinitionSource; | |
| 34 | import com.scrivenvar.definition.MapInterpolator; | |
| 35 | import com.scrivenvar.definition.yaml.YamlDefinitionSource; | |
| 36 | import com.scrivenvar.editors.DefinitionNameInjector; | |
| 37 | import com.scrivenvar.editors.EditorPane; | |
| 38 | import com.scrivenvar.editors.markdown.MarkdownEditorPane; | |
| 39 | import com.scrivenvar.preferences.UserPreferences; | |
| 40 | import com.scrivenvar.preview.HTMLPreviewPane; | |
| 41 | import com.scrivenvar.processors.HtmlPreviewProcessor; | |
| 42 | import com.scrivenvar.processors.Processor; | |
| 43 | import com.scrivenvar.processors.ProcessorFactory; | |
| 44 | import com.scrivenvar.service.Options; | |
| 45 | import com.scrivenvar.service.Snitch; | |
| 46 | import com.scrivenvar.spelling.api.SpellCheckListener; | |
| 47 | import com.scrivenvar.spelling.api.SpellChecker; | |
| 48 | import com.scrivenvar.spelling.impl.PermissiveSpeller; | |
| 49 | import com.scrivenvar.spelling.impl.SymSpellSpeller; | |
| 50 | import com.scrivenvar.util.Action; | |
| 51 | import com.scrivenvar.util.ActionBuilder; | |
| 52 | import com.scrivenvar.util.ActionUtils; | |
| 53 | import com.vladsch.flexmark.parser.Parser; | |
| 54 | import com.vladsch.flexmark.util.ast.NodeVisitor; | |
| 55 | import com.vladsch.flexmark.util.ast.VisitHandler; | |
| 56 | import javafx.beans.binding.Bindings; | |
| 57 | import javafx.beans.binding.BooleanBinding; | |
| 58 | import javafx.beans.property.BooleanProperty; | |
| 59 | import javafx.beans.property.SimpleBooleanProperty; | |
| 60 | import javafx.beans.value.ChangeListener; | |
| 61 | import javafx.beans.value.ObservableBooleanValue; | |
| 62 | import javafx.beans.value.ObservableValue; | |
| 63 | import javafx.collections.ListChangeListener.Change; | |
| 64 | import javafx.collections.ObservableList; | |
| 65 | import javafx.event.Event; | |
| 66 | import javafx.event.EventHandler; | |
| 67 | import javafx.geometry.Pos; | |
| 68 | import javafx.scene.Node; | |
| 69 | import javafx.scene.Scene; | |
| 70 | import javafx.scene.control.*; | |
| 71 | import javafx.scene.control.Alert.AlertType; | |
| 72 | import javafx.scene.image.Image; | |
| 73 | import javafx.scene.image.ImageView; | |
| 74 | import javafx.scene.input.Clipboard; | |
| 75 | import javafx.scene.input.ClipboardContent; | |
| 76 | import javafx.scene.input.KeyEvent; | |
| 77 | import javafx.scene.layout.BorderPane; | |
| 78 | import javafx.scene.layout.VBox; | |
| 79 | import javafx.scene.text.Text; | |
| 80 | import javafx.stage.Window; | |
| 81 | import javafx.stage.WindowEvent; | |
| 82 | import javafx.util.Duration; | |
| 83 | import org.apache.commons.lang3.SystemUtils; | |
| 84 | import org.controlsfx.control.StatusBar; | |
| 85 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 86 | import org.fxmisc.richtext.model.StyleSpansBuilder; | |
| 87 | import org.reactfx.value.Val; | |
| 88 | ||
| 89 | import java.io.BufferedReader; | |
| 90 | import java.io.FileNotFoundException; | |
| 91 | import java.io.InputStreamReader; | |
| 92 | import java.nio.file.Path; | |
| 93 | import java.util.*; | |
| 94 | import java.util.concurrent.atomic.AtomicInteger; | |
| 95 | import java.util.function.Consumer; | |
| 96 | import java.util.function.Function; | |
| 97 | import java.util.prefs.Preferences; | |
| 98 | import java.util.stream.Collectors; | |
| 99 | ||
| 100 | import static com.scrivenvar.Constants.*; | |
| 101 | import static com.scrivenvar.Messages.get; | |
| 102 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 103 | import static com.scrivenvar.util.StageState.*; | |
| 104 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 105 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 106 | import static java.util.Collections.emptyList; | |
| 107 | import static java.util.Collections.singleton; | |
| 108 | import static javafx.application.Platform.runLater; | |
| 109 | import static javafx.event.Event.fireEvent; | |
| 110 | import static javafx.scene.input.KeyCode.ENTER; | |
| 111 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 112 | import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward; | |
| 113 | ||
| 114 | /** | |
| 115 | * Main window containing a tab pane in the center for file editors. | |
| 116 | */ | |
| 117 | public class MainWindow implements Observer { | |
| 118 | /** | |
| 119 | * The {@code OPTIONS} variable must be declared before all other variables | |
| 120 | * to prevent subsequent initializations from failing due to missing user | |
| 121 | * preferences. | |
| 122 | */ | |
| 123 | private static final Options sOptions = Services.load( Options.class ); | |
| 124 | private static final Snitch SNITCH = Services.load( Snitch.class ); | |
| 125 | ||
| 126 | private final Scene mScene; | |
| 127 | private final StatusBar mStatusBar; | |
| 128 | private final Text mLineNumberText; | |
| 129 | private final TextField mFindTextField; | |
| 130 | private final SpellChecker mSpellChecker; | |
| 131 | ||
| 132 | private final Object mMutex = new Object(); | |
| 133 | ||
| 134 | /** | |
| 135 | * Prevents re-instantiation of processing classes. | |
| 136 | */ | |
| 137 | private final Map<FileEditorTab, Processor<String>> mProcessors = | |
| 138 | new HashMap<>(); | |
| 139 | ||
| 140 | private final Map<String, String> mResolvedMap = | |
| 141 | new HashMap<>( DEFAULT_MAP_SIZE ); | |
| 142 | ||
| 143 | private final EventHandler<PreferencesFxEvent> mRPreferencesListener = | |
| 144 | event -> rerender(); | |
| 145 | ||
| 146 | /** | |
| 147 | * Called when the definition data is changed. | |
| 148 | */ | |
| 149 | private final EventHandler<TreeItem.TreeModificationEvent<Event>> | |
| 150 | mTreeHandler = event -> { | |
| 151 | exportDefinitions( getDefinitionPath() ); | |
| 152 | interpolateResolvedMap(); | |
| 153 | rerender(); | |
| 154 | }; | |
| 155 | ||
| 156 | /** | |
| 157 | * Called to inject the selected item when the user presses ENTER in the | |
| 158 | * definition pane. | |
| 159 | */ | |
| 160 | private final EventHandler<? super KeyEvent> mDefinitionKeyHandler = | |
| 161 | event -> { | |
| 162 | if( event.getCode() == ENTER ) { | |
| 163 | getDefinitionNameInjector().injectSelectedItem(); | |
| 164 | } | |
| 165 | }; | |
| 166 | ||
| 167 | private final ChangeListener<Integer> mCaretPositionListener = | |
| 168 | ( observable, oldPosition, newPosition ) -> { | |
| 169 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 170 | final EditorPane pane = tab.getEditorPane(); | |
| 171 | final StyleClassedTextArea editor = pane.getEditor(); | |
| 172 | ||
| 173 | getLineNumberText().setText( | |
| 174 | get( STATUS_BAR_LINE, | |
| 175 | editor.getCurrentParagraph() + 1, | |
| 176 | editor.getParagraphs().size(), | |
| 177 | editor.getCaretPosition() | |
| 178 | ) | |
| 179 | ); | |
| 180 | }; | |
| 181 | ||
| 182 | private final ChangeListener<Integer> mCaretParagraphListener = | |
| 183 | ( observable, oldIndex, newIndex ) -> | |
| 184 | scrollToParagraph( newIndex, true ); | |
| 185 | ||
| 186 | private DefinitionSource mDefinitionSource = createDefaultDefinitionSource(); | |
| 187 | private final DefinitionPane mDefinitionPane = createDefinitionPane(); | |
| 188 | private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane(); | |
| 189 | private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane( | |
| 190 | mCaretPositionListener, | |
| 191 | mCaretParagraphListener ); | |
| 192 | ||
| 193 | /** | |
| 194 | * Listens on the definition pane for double-click events. | |
| 195 | */ | |
| 196 | private final DefinitionNameInjector mDefinitionNameInjector | |
| 197 | = new DefinitionNameInjector( mDefinitionPane ); | |
| 198 | ||
| 199 | public MainWindow() { | |
| 200 | mStatusBar = createStatusBar(); | |
| 201 | mLineNumberText = createLineNumberText(); | |
| 202 | mFindTextField = createFindTextField(); | |
| 203 | mScene = createScene(); | |
| 204 | mSpellChecker = createSpellChecker(); | |
| 205 | ||
| 206 | // Add the close request listener before the window is shown. | |
| 207 | initLayout(); | |
| 208 | StatusBarNotifier.setStatusBar( mStatusBar ); | |
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Called after the stage is shown. | |
| 213 | */ | |
| 214 | public void init() { | |
| 215 | initFindInput(); | |
| 216 | initSnitch(); | |
| 217 | initDefinitionListener(); | |
| 218 | initTabAddedListener(); | |
| 219 | initTabChangedListener(); | |
| 220 | initPreferences(); | |
| 221 | initVariableNameInjector(); | |
| 222 | } | |
| 223 | ||
| 224 | private void initLayout() { | |
| 225 | final var scene = getScene(); | |
| 226 | ||
| 227 | scene.getStylesheets().add( STYLESHEET_SCENE ); | |
| 228 | scene.windowProperty().addListener( | |
| 229 | ( unused, oldWindow, newWindow ) -> | |
| 230 | newWindow.setOnCloseRequest( | |
| 231 | e -> { | |
| 232 | if( !getFileEditorPane().closeAllEditors() ) { | |
| 233 | e.consume(); | |
| 234 | } | |
| 235 | } | |
| 236 | ) | |
| 237 | ); | |
| 238 | } | |
| 239 | ||
| 240 | /** | |
| 241 | * Initialize the find input text field to listen on F3, ENTER, and | |
| 242 | * ESCAPE key presses. | |
| 243 | */ | |
| 244 | private void initFindInput() { | |
| 245 | final TextField input = getFindTextField(); | |
| 246 | ||
| 247 | input.setOnKeyPressed( ( KeyEvent event ) -> { | |
| 248 | switch( event.getCode() ) { | |
| 249 | case F3: | |
| 250 | case ENTER: | |
| 251 | editFindNext(); | |
| 252 | break; | |
| 253 | case F: | |
| 254 | if( !event.isControlDown() ) { | |
| 255 | break; | |
| 256 | } | |
| 257 | case ESCAPE: | |
| 258 | getStatusBar().setGraphic( null ); | |
| 259 | getActiveFileEditorTab().getEditorPane().requestFocus(); | |
| 260 | break; | |
| 261 | } | |
| 262 | } ); | |
| 263 | ||
| 264 | // Remove when the input field loses focus. | |
| 265 | input.focusedProperty().addListener( | |
| 266 | ( focused, oldFocus, newFocus ) -> { | |
| 267 | if( !newFocus ) { | |
| 268 | getStatusBar().setGraphic( null ); | |
| 269 | } | |
| 270 | } | |
| 271 | ); | |
| 272 | } | |
| 273 | ||
| 274 | /** | |
| 275 | * Watch for changes to external files. In particular, this awaits | |
| 276 | * modifications to any XSL files associated with XML files being edited. | |
| 277 | * When | |
| 278 | * an XSL file is modified (external to the application), the snitch's ears | |
| 279 | * perk up and the file is reloaded. This keeps the XSL transformation up to | |
| 280 | * date with what's on the file system. | |
| 281 | */ | |
| 282 | private void initSnitch() { | |
| 283 | SNITCH.addObserver( this ); | |
| 284 | } | |
| 285 | ||
| 286 | /** | |
| 287 | * Listen for {@link FileEditorTabPane} to receive open definition file | |
| 288 | * event. | |
| 289 | */ | |
| 290 | private void initDefinitionListener() { | |
| 291 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 292 | ( final ObservableValue<? extends Path> file, | |
| 293 | final Path oldPath, final Path newPath ) -> { | |
| 294 | openDefinitions( newPath ); | |
| 295 | rerender(); | |
| 296 | } | |
| 297 | ); | |
| 298 | } | |
| 299 | ||
| 300 | /** | |
| 301 | * Re-instantiates all processors then re-renders the active tab. This | |
| 302 | * will refresh the resolved map, force R to re-initialize, and brute-force | |
| 303 | * XSLT file reloads. | |
| 304 | */ | |
| 305 | private void rerender() { | |
| 306 | runLater( | |
| 307 | () -> { | |
| 308 | resetProcessors(); | |
| 309 | renderActiveTab(); | |
| 310 | } | |
| 311 | ); | |
| 312 | } | |
| 313 | ||
| 314 | /** | |
| 315 | * When tabs are added, hook the various change listeners onto the new | |
| 316 | * tab sothat the preview pane refreshes as necessary. | |
| 317 | */ | |
| 318 | private void initTabAddedListener() { | |
| 319 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 320 | ||
| 321 | // Make sure the text processor kicks off when new files are opened. | |
| 322 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 323 | ||
| 324 | // Update the preview pane on tab changes. | |
| 325 | tabs.addListener( | |
| 326 | ( final Change<? extends Tab> change ) -> { | |
| 327 | while( change.next() ) { | |
| 328 | if( change.wasAdded() ) { | |
| 329 | // Multiple tabs can be added simultaneously. | |
| 330 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 331 | final FileEditorTab tab = (FileEditorTab) newTab; | |
| 332 | ||
| 333 | initTextChangeListener( tab ); | |
| 334 | initScrollEventListener( tab ); | |
| 335 | initSpellCheckListener( tab ); | |
| 336 | // initSyntaxListener( tab ); | |
| 337 | } | |
| 338 | } | |
| 339 | } | |
| 340 | } | |
| 341 | ); | |
| 342 | } | |
| 343 | ||
| 344 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 345 | tab.addTextChangeListener( | |
| 346 | ( __, ov, nv ) -> { | |
| 347 | process( tab ); | |
| 348 | scrollToParagraph( getCurrentParagraphIndex() ); | |
| 349 | } | |
| 350 | ); | |
| 351 | } | |
| 352 | ||
| 353 | private void initScrollEventListener( final FileEditorTab tab ) { | |
| 354 | final var scrollPane = tab.getScrollPane(); | |
| 355 | final var scrollBar = getPreviewPane().getVerticalScrollBar(); | |
| 356 | ||
| 357 | addShowListener( scrollPane, ( __ ) -> { | |
| 358 | final var handler = new ScrollEventHandler( scrollPane, scrollBar ); | |
| 359 | handler.enabledProperty().bind( tab.selectedProperty() ); | |
| 360 | } ); | |
| 361 | } | |
| 362 | ||
| 363 | /** | |
| 364 | * Listen for changes to the any particular paragraph and perform a quick | |
| 365 | * spell check upon it. The style classes in the editor will be changed to | |
| 366 | * mark any spelling mistakes in the paragraph. The user may then interact | |
| 367 | * with any misspelled word (i.e., any piece of text that is marked) to | |
| 368 | * revise the spelling. | |
| 369 | * | |
| 370 | * @param tab The tab to spellcheck. | |
| 371 | */ | |
| 372 | private void initSpellCheckListener( final FileEditorTab tab ) { | |
| 373 | final var editor = tab.getEditorPane().getEditor(); | |
| 374 | ||
| 375 | // When the editor first appears, run a full spell check. This allows | |
| 376 | // spell checking while typing to be restricted to the active paragraph, | |
| 377 | // which is usually substantially smaller than the whole document. | |
| 378 | addShowListener( | |
| 379 | editor, ( __ ) -> spellcheck( editor, editor.getText() ) | |
| 380 | ); | |
| 381 | ||
| 382 | // Use the plain text changes so that notifications of style changes | |
| 383 | // are suppressed. Checking against the identity ensures that only | |
| 384 | // new text additions or deletions trigger proofreading. | |
| 385 | editor.plainTextChanges() | |
| 386 | .filter( p -> !p.isIdentity() ).subscribe( change -> { | |
| 387 | ||
| 388 | // Only perform a spell check on the current paragraph. The | |
| 389 | // entire document is processed once, when opened. | |
| 390 | final var offset = change.getPosition(); | |
| 391 | final var position = editor.offsetToPosition( offset, Forward ); | |
| 392 | final var paraId = position.getMajor(); | |
| 393 | final var paragraph = editor.getParagraph( paraId ); | |
| 394 | final var text = paragraph.getText(); | |
| 395 | ||
| 396 | // Ensure that styles aren't doubled-up. | |
| 397 | editor.clearStyle( paraId ); | |
| 398 | ||
| 399 | spellcheck( editor, text, paraId ); | |
| 400 | } ); | |
| 401 | } | |
| 402 | ||
| 403 | /** | |
| 404 | * Listen for new tab selection events. | |
| 405 | */ | |
| 406 | private void initTabChangedListener() { | |
| 407 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 408 | ||
| 409 | // Update the preview pane changing tabs. | |
| 410 | editorPane.addTabSelectionListener( | |
| 411 | ( tabPane, oldTab, newTab ) -> { | |
| 412 | if( newTab == null ) { | |
| 413 | // Clear the preview pane when closing an editor. When the last | |
| 414 | // tab is closed, this ensures that the preview pane is empty. | |
| 415 | getPreviewPane().clear(); | |
| 416 | } | |
| 417 | else { | |
| 418 | final var tab = (FileEditorTab) newTab; | |
| 419 | updateVariableNameInjector( tab ); | |
| 420 | process( tab ); | |
| 421 | } | |
| 422 | } | |
| 423 | ); | |
| 424 | } | |
| 425 | ||
| 426 | /** | |
| 427 | * Reloads the preferences from the previous session. | |
| 428 | */ | |
| 429 | private void initPreferences() { | |
| 430 | initDefinitionPane(); | |
| 431 | getFileEditorPane().initPreferences(); | |
| 432 | getUserPreferences().addSaveEventHandler( mRPreferencesListener ); | |
| 433 | } | |
| 434 | ||
| 435 | private void initVariableNameInjector() { | |
| 436 | updateVariableNameInjector( getActiveFileEditorTab() ); | |
| 437 | } | |
| 438 | ||
| 439 | /** | |
| 440 | * Calls the listener when the given node is shown for the first time. The | |
| 441 | * visible property is not the same as the initial showing event; visibility | |
| 442 | * can be triggered numerous times (such as going off screen). | |
| 443 | * <p> | |
| 444 | * This is called, for example, before the drag handler can be attached, | |
| 445 | * because the scrollbar for the text editor pane must be visible. | |
| 446 | * </p> | |
| 447 | * | |
| 448 | * @param node The node to watch for showing. | |
| 449 | * @param consumer The consumer to invoke when the event fires. | |
| 450 | */ | |
| 451 | private void addShowListener( | |
| 452 | final Node node, final Consumer<Void> consumer ) { | |
| 453 | final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) -> | |
| 454 | runLater( () -> { | |
| 455 | if( newShow != null && newShow ) { | |
| 456 | try { | |
| 457 | consumer.accept( null ); | |
| 458 | } catch( final Exception ex ) { | |
| 459 | alert( ex ); | |
| 460 | } | |
| 461 | } | |
| 462 | } ); | |
| 463 | ||
| 464 | Val.flatMap( node.sceneProperty(), Scene::windowProperty ) | |
| 465 | .flatMap( Window::showingProperty ) | |
| 466 | .addListener( listener ); | |
| 467 | } | |
| 468 | ||
| 469 | private void scrollToParagraph( final int id ) { | |
| 470 | scrollToParagraph( id, false ); | |
| 471 | } | |
| 472 | ||
| 473 | /** | |
| 474 | * @param id The paragraph to scroll to, will be approximated if it doesn't | |
| 475 | * exist. | |
| 476 | * @param force {@code true} means to force scrolling immediately, which | |
| 477 | * should only be attempted when it is known that the document | |
| 478 | * has been fully rendered. Otherwise the internal map of ID | |
| 479 | * attributes will be incomplete and scrolling will flounder. | |
| 480 | */ | |
| 481 | private void scrollToParagraph( final int id, final boolean force ) { | |
| 482 | synchronized( mMutex ) { | |
| 483 | final var previewPane = getPreviewPane(); | |
| 484 | final var scrollPane = previewPane.getScrollPane(); | |
| 485 | final int approxId = getActiveEditorPane().approximateParagraphId( id ); | |
| 486 | ||
| 487 | if( force ) { | |
| 488 | previewPane.scrollTo( approxId ); | |
| 489 | } | |
| 490 | else { | |
| 491 | previewPane.tryScrollTo( approxId ); | |
| 492 | } | |
| 493 | ||
| 494 | scrollPane.repaint(); | |
| 495 | } | |
| 496 | } | |
| 497 | ||
| 498 | private void updateVariableNameInjector( final FileEditorTab tab ) { | |
| 499 | getDefinitionNameInjector().addListener( tab ); | |
| 500 | } | |
| 501 | ||
| 502 | /** | |
| 503 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 504 | * tab. This can be called when the text changes, the caret paragraph | |
| 505 | * changes, or the file tab changes. | |
| 506 | * | |
| 507 | * @param tab The file editor tab that has been changed in some fashion. | |
| 508 | */ | |
| 509 | private void process( final FileEditorTab tab ) { | |
| 510 | if( tab != null ) { | |
| 511 | getPreviewPane().setPath( tab.getPath() ); | |
| 512 | ||
| 513 | final Processor<String> processor = getProcessors().computeIfAbsent( | |
| 514 | tab, p -> createProcessors( tab ) | |
| 515 | ); | |
| 516 | ||
| 517 | try { | |
| 518 | processChain( processor, tab.getEditorText() ); | |
| 519 | } catch( final Exception ex ) { | |
| 520 | alert( ex ); | |
| 521 | } | |
| 522 | } | |
| 523 | } | |
| 524 | ||
| 525 | /** | |
| 526 | * Executes the processing chain, operating on the given string. | |
| 527 | * | |
| 528 | * @param handler The first processor in the chain to call. | |
| 529 | * @param text The initial value of the text to process. | |
| 530 | * @return The final value of the text that was processed by the chain. | |
| 531 | */ | |
| 532 | private String processChain( Processor<String> handler, String text ) { | |
| 533 | while( handler != null && text != null ) { | |
| 534 | text = handler.apply( text ); | |
| 535 | handler = handler.next(); | |
| 536 | } | |
| 537 | ||
| 538 | return text; | |
| 539 | } | |
| 540 | ||
| 541 | private void renderActiveTab() { | |
| 542 | process( getActiveFileEditorTab() ); | |
| 543 | } | |
| 544 | ||
| 545 | /** | |
| 546 | * Called when a definition source is opened. | |
| 547 | * | |
| 548 | * @param path Path to the definition source that was opened. | |
| 549 | */ | |
| 550 | private void openDefinitions( final Path path ) { | |
| 551 | try { | |
| 552 | final var ds = createDefinitionSource( path ); | |
| 553 | setDefinitionSource( ds ); | |
| 554 | ||
| 555 | final var prefs = getUserPreferences(); | |
| 556 | prefs.definitionPathProperty().setValue( path.toFile() ); | |
| 557 | prefs.save(); | |
| 558 | ||
| 559 | final var tooltipPath = new Tooltip( path.toString() ); | |
| 560 | tooltipPath.setShowDelay( Duration.millis( 200 ) ); | |
| 561 | ||
| 562 | final var pane = getDefinitionPane(); | |
| 563 | pane.update( ds ); | |
| 564 | pane.addTreeChangeHandler( mTreeHandler ); | |
| 565 | pane.addKeyEventHandler( mDefinitionKeyHandler ); | |
| 566 | pane.filenameProperty().setValue( path.getFileName().toString() ); | |
| 567 | pane.setTooltip( tooltipPath ); | |
| 568 | ||
| 569 | interpolateResolvedMap(); | |
| 570 | } catch( final Exception ex ) { | |
| 571 | alert( ex ); | |
| 572 | } | |
| 573 | } | |
| 574 | ||
| 575 | private void exportDefinitions( final Path path ) { | |
| 576 | try { | |
| 577 | final var pane = getDefinitionPane(); | |
| 578 | final var root = pane.getTreeView().getRoot(); | |
| 579 | final var problemChild = pane.isTreeWellFormed(); | |
| 580 | ||
| 581 | if( problemChild == null ) { | |
| 582 | getDefinitionSource().getTreeAdapter().export( root, path ); | |
| 583 | } | |
| 584 | else { | |
| 585 | alert( "yaml.error.tree.form", problemChild.getValue() ); | |
| 586 | } | |
| 587 | } catch( final Exception ex ) { | |
| 588 | alert( ex ); | |
| 589 | } | |
| 590 | } | |
| 591 | ||
| 592 | private void interpolateResolvedMap() { | |
| 593 | final var treeMap = getDefinitionPane().toMap(); | |
| 594 | final var map = new HashMap<>( treeMap ); | |
| 595 | MapInterpolator.interpolate( map ); | |
| 596 | ||
| 597 | getResolvedMap().clear(); | |
| 598 | getResolvedMap().putAll( map ); | |
| 599 | } | |
| 600 | ||
| 601 | private void initDefinitionPane() { | |
| 602 | openDefinitions( getDefinitionPath() ); | |
| 603 | } | |
| 604 | ||
| 605 | //---- File actions ------------------------------------------------------- | |
| 606 | ||
| 607 | /** | |
| 608 | * Called when an {@link Observable} instance has changed. This is called | |
| 609 | * by both the {@link Snitch} service and the notify service. The @link | |
| 610 | * Snitch} service can be called for different file types, including | |
| 611 | * {@link DefinitionSource} instances. | |
| 612 | * | |
| 613 | * @param observable The observed instance. | |
| 614 | * @param value The noteworthy item. | |
| 615 | */ | |
| 616 | @Override | |
| 617 | public void update( final Observable observable, final Object value ) { | |
| 618 | if( value instanceof Path && observable instanceof Snitch ) { | |
| 619 | updateSelectedTab(); | |
| 620 | } | |
| 621 | } | |
| 622 | ||
| 623 | /** | |
| 624 | * Called when a file has been modified. | |
| 625 | */ | |
| 626 | private void updateSelectedTab() { | |
| 627 | rerender(); | |
| 628 | } | |
| 629 | ||
| 630 | /** | |
| 631 | * After resetting the processors, they will refresh anew to be up-to-date | |
| 632 | * with the files (text and definition) currently loaded into the editor. | |
| 633 | */ | |
| 634 | private void resetProcessors() { | |
| 635 | getProcessors().clear(); | |
| 636 | } | |
| 637 | ||
| 638 | //---- File actions ------------------------------------------------------- | |
| 639 | ||
| 640 | private void fileNew() { | |
| 641 | getFileEditorPane().newEditor(); | |
| 642 | } | |
| 643 | ||
| 644 | private void fileOpen() { | |
| 645 | getFileEditorPane().openFileDialog(); | |
| 646 | } | |
| 647 | ||
| 648 | private void fileClose() { | |
| 649 | getFileEditorPane().closeEditor( getActiveFileEditorTab(), true ); | |
| 650 | } | |
| 651 | ||
| 652 | /** | |
| 653 | * TODO: Upon closing, first remove the tab change listeners. (There's no | |
| 654 | * need to re-render each tab when all are being closed.) | |
| 655 | */ | |
| 656 | private void fileCloseAll() { | |
| 657 | getFileEditorPane().closeAllEditors(); | |
| 658 | } | |
| 659 | ||
| 660 | private void fileSave() { | |
| 661 | getFileEditorPane().saveEditor( getActiveFileEditorTab() ); | |
| 662 | } | |
| 663 | ||
| 664 | private void fileSaveAs() { | |
| 665 | final FileEditorTab editor = getActiveFileEditorTab(); | |
| 666 | getFileEditorPane().saveEditorAs( editor ); | |
| 667 | getProcessors().remove( editor ); | |
| 668 | ||
| 669 | try { | |
| 670 | process( editor ); | |
| 671 | } catch( final Exception ex ) { | |
| 672 | alert( ex ); | |
| 673 | } | |
| 674 | } | |
| 675 | ||
| 676 | private void fileSaveAll() { | |
| 677 | getFileEditorPane().saveAllEditors(); | |
| 678 | } | |
| 679 | ||
| 680 | private void fileExit() { | |
| 681 | final Window window = getWindow(); | |
| 682 | fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | |
| 683 | } | |
| 684 | ||
| 685 | //---- Edit actions ------------------------------------------------------- | |
| 686 | ||
| 687 | /** | |
| 688 | * Transform the Markdown into HTML then copy that HTML into the copy | |
| 689 | * buffer. | |
| 690 | */ | |
| 691 | private void copyHtml() { | |
| 692 | final var markdown = getActiveEditorPane().getText(); | |
| 693 | final var processors = createProcessorFactory().createProcessors( | |
| 694 | getActiveFileEditorTab() | |
| 695 | ); | |
| 696 | ||
| 697 | final var chain = processors.remove( HtmlPreviewProcessor.class ); | |
| 698 | ||
| 699 | final String html = processChain( chain, markdown ); | |
| 700 | ||
| 701 | final Clipboard clipboard = Clipboard.getSystemClipboard(); | |
| 702 | final ClipboardContent content = new ClipboardContent(); | |
| 703 | content.putString( html ); | |
| 704 | clipboard.setContent( content ); | |
| 705 | } | |
| 706 | ||
| 707 | /** | |
| 708 | * Used to find text in the active file editor window. | |
| 709 | */ | |
| 710 | private void editFind() { | |
| 711 | final TextField input = getFindTextField(); | |
| 712 | getStatusBar().setGraphic( input ); | |
| 713 | input.requestFocus(); | |
| 714 | } | |
| 715 | ||
| 716 | public void editFindNext() { | |
| 717 | getActiveFileEditorTab().searchNext( getFindTextField().getText() ); | |
| 718 | } | |
| 719 | ||
| 720 | public void editPreferences() { | |
| 721 | getUserPreferences().show(); | |
| 722 | } | |
| 723 | ||
| 724 | //---- Insert actions ----------------------------------------------------- | |
| 725 | ||
| 726 | /** | |
| 727 | * Delegates to the active editor to handle wrapping the current text | |
| 728 | * selection with leading and trailing strings. | |
| 729 | * | |
| 730 | * @param leading The string to put before the selection. | |
| 731 | * @param trailing The string to put after the selection. | |
| 732 | */ | |
| 733 | private void insertMarkdown( | |
| 734 | final String leading, final String trailing ) { | |
| 735 | getActiveEditorPane().surroundSelection( leading, trailing ); | |
| 736 | } | |
| 737 | ||
| 738 | private void insertMarkdown( | |
| 739 | final String leading, final String trailing, final String hint ) { | |
| 740 | getActiveEditorPane().surroundSelection( leading, trailing, hint ); | |
| 741 | } | |
| 742 | ||
| 743 | //---- View actions ------------------------------------------------------- | |
| 744 | ||
| 745 | private void viewRefresh() { | |
| 746 | rerender(); | |
| 747 | } | |
| 748 | ||
| 749 | //---- Help actions ------------------------------------------------------- | |
| 750 | ||
| 751 | private void helpAbout() { | |
| 752 | final Alert alert = new Alert( AlertType.INFORMATION ); | |
| 753 | alert.setTitle( get( "Dialog.about.title" ) ); | |
| 754 | alert.setHeaderText( get( "Dialog.about.header" ) ); | |
| 755 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 756 | alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | |
| 757 | alert.initOwner( getWindow() ); | |
| 758 | ||
| 759 | alert.showAndWait(); | |
| 760 | } | |
| 761 | ||
| 762 | //---- Member creators ---------------------------------------------------- | |
| 763 | ||
| 764 | private SpellChecker createSpellChecker() { | |
| 765 | try { | |
| 766 | final Collection<String> lexicon = readLexicon( "en.txt" ); | |
| 767 | return SymSpellSpeller.forLexicon( lexicon ); | |
| 768 | } catch( final Exception ex ) { | |
| 769 | alert( ex ); | |
| 770 | return new PermissiveSpeller(); | |
| 771 | } | |
| 772 | } | |
| 773 | ||
| 774 | /** | |
| 775 | * Factory to create processors that are suited to different file types. | |
| 776 | * | |
| 777 | * @param tab The tab that is subjected to processing. | |
| 778 | * @return A processor suited to the file type specified by the tab's path. | |
| 779 | */ | |
| 780 | private Processor<String> createProcessors( final FileEditorTab tab ) { | |
| 781 | return createProcessorFactory().createProcessors( tab ); | |
| 782 | } | |
| 783 | ||
| 784 | private ProcessorFactory createProcessorFactory() { | |
| 785 | return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); | |
| 786 | } | |
| 787 | ||
| 788 | private DefinitionPane createDefinitionPane() { | |
| 789 | return new DefinitionPane(); | |
| 790 | } | |
| 791 | ||
| 792 | private HTMLPreviewPane createHTMLPreviewPane() { | |
| 793 | return new HTMLPreviewPane(); | |
| 794 | } | |
| 795 | ||
| 796 | private DefinitionSource createDefaultDefinitionSource() { | |
| 797 | return new YamlDefinitionSource( getDefinitionPath() ); | |
| 798 | } | |
| 799 | ||
| 800 | private DefinitionSource createDefinitionSource( final Path path ) { | |
| 801 | try { | |
| 802 | return createDefinitionFactory().createDefinitionSource( path ); | |
| 803 | } catch( final Exception ex ) { | |
| 804 | alert( ex ); | |
| 805 | return createDefaultDefinitionSource(); | |
| 806 | } | |
| 807 | } | |
| 808 | ||
| 809 | private TextField createFindTextField() { | |
| 810 | return new TextField(); | |
| 811 | } | |
| 812 | ||
| 813 | private DefinitionFactory createDefinitionFactory() { | |
| 814 | return new DefinitionFactory(); | |
| 815 | } | |
| 816 | ||
| 817 | private StatusBar createStatusBar() { | |
| 818 | return new StatusBar(); | |
| 819 | } | |
| 820 | ||
| 821 | private Scene createScene() { | |
| 822 | final SplitPane splitPane = new SplitPane( | |
| 823 | getDefinitionPane(), | |
| 824 | getFileEditorPane(), | |
| 825 | getPreviewPane() ); | |
| 826 | ||
| 827 | splitPane.setDividerPositions( | |
| 828 | getFloat( K_PANE_SPLIT_DEFINITION, .22f ), | |
| 829 | getFloat( K_PANE_SPLIT_EDITOR, .60f ), | |
| 830 | getFloat( K_PANE_SPLIT_PREVIEW, .18f ) ); | |
| 831 | ||
| 832 | getDefinitionPane().prefHeightProperty() | |
| 833 | .bind( splitPane.heightProperty() ); | |
| 834 | ||
| 835 | final BorderPane borderPane = new BorderPane(); | |
| 836 | borderPane.setPrefSize( 1280, 800 ); | |
| 837 | borderPane.setTop( createMenuBar() ); | |
| 838 | borderPane.setBottom( getStatusBar() ); | |
| 839 | borderPane.setCenter( splitPane ); | |
| 840 | ||
| 841 | final VBox statusBar = new VBox(); | |
| 842 | statusBar.setAlignment( Pos.BASELINE_CENTER ); | |
| 843 | statusBar.getChildren().add( getLineNumberText() ); | |
| 844 | getStatusBar().getRightItems().add( statusBar ); | |
| 845 | ||
| 846 | // Force preview pane refresh on Windows. | |
| 847 | if( SystemUtils.IS_OS_WINDOWS ) { | |
| 848 | splitPane.getDividers().get( 1 ).positionProperty().addListener( | |
| 849 | ( l, oValue, nValue ) -> runLater( | |
| 850 | () -> getPreviewPane().getScrollPane().repaint() | |
| 851 | ) | |
| 852 | ); | |
| 853 | } | |
| 854 | ||
| 855 | return new Scene( borderPane ); | |
| 856 | } | |
| 857 | ||
| 858 | private Text createLineNumberText() { | |
| 859 | return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) ); | |
| 860 | } | |
| 861 | ||
| 862 | private Node createMenuBar() { | |
| 863 | final BooleanBinding activeFileEditorIsNull = | |
| 864 | getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 865 | ||
| 866 | // File actions | |
| 867 | final Action fileNewAction = new ActionBuilder() | |
| 868 | .setText( "Main.menu.file.new" ) | |
| 869 | .setAccelerator( "Shortcut+N" ) | |
| 870 | .setIcon( FILE_ALT ) | |
| 871 | .setAction( e -> fileNew() ) | |
| 872 | .build(); | |
| 873 | final Action fileOpenAction = new ActionBuilder() | |
| 874 | .setText( "Main.menu.file.open" ) | |
| 875 | .setAccelerator( "Shortcut+O" ) | |
| 876 | .setIcon( FOLDER_OPEN_ALT ) | |
| 877 | .setAction( e -> fileOpen() ) | |
| 878 | .build(); | |
| 879 | final Action fileCloseAction = new ActionBuilder() | |
| 880 | .setText( "Main.menu.file.close" ) | |
| 881 | .setAccelerator( "Shortcut+W" ) | |
| 882 | .setAction( e -> fileClose() ) | |
| 883 | .setDisable( activeFileEditorIsNull ) | |
| 884 | .build(); | |
| 885 | final Action fileCloseAllAction = new ActionBuilder() | |
| 886 | .setText( "Main.menu.file.close_all" ) | |
| 887 | .setAction( e -> fileCloseAll() ) | |
| 888 | .setDisable( activeFileEditorIsNull ) | |
| 889 | .build(); | |
| 890 | final Action fileSaveAction = new ActionBuilder() | |
| 891 | .setText( "Main.menu.file.save" ) | |
| 892 | .setAccelerator( "Shortcut+S" ) | |
| 893 | .setIcon( FLOPPY_ALT ) | |
| 894 | .setAction( e -> fileSave() ) | |
| 895 | .setDisable( createActiveBooleanProperty( | |
| 896 | FileEditorTab::modifiedProperty ).not() ) | |
| 897 | .build(); | |
| 898 | final Action fileSaveAsAction = new ActionBuilder() | |
| 899 | .setText( "Main.menu.file.save_as" ) | |
| 900 | .setAction( e -> fileSaveAs() ) | |
| 901 | .setDisable( activeFileEditorIsNull ) | |
| 902 | .build(); | |
| 903 | final Action fileSaveAllAction = new ActionBuilder() | |
| 904 | .setText( "Main.menu.file.save_all" ) | |
| 905 | .setAccelerator( "Shortcut+Shift+S" ) | |
| 906 | .setAction( e -> fileSaveAll() ) | |
| 907 | .setDisable( Bindings.not( | |
| 908 | getFileEditorPane().anyFileEditorModifiedProperty() ) ) | |
| 909 | .build(); | |
| 910 | final Action fileExitAction = new ActionBuilder() | |
| 911 | .setText( "Main.menu.file.exit" ) | |
| 912 | .setAction( e -> fileExit() ) | |
| 913 | .build(); | |
| 914 | ||
| 915 | // Edit actions | |
| 916 | final Action editCopyHtmlAction = new ActionBuilder() | |
| 917 | .setText( "Main.menu.edit.copy.html" ) | |
| 918 | .setIcon( HTML5 ) | |
| 919 | .setAction( e -> copyHtml() ) | |
| 920 | .setDisable( activeFileEditorIsNull ) | |
| 921 | .build(); | |
| 922 | ||
| 923 | final Action editUndoAction = new ActionBuilder() | |
| 924 | .setText( "Main.menu.edit.undo" ) | |
| 925 | .setAccelerator( "Shortcut+Z" ) | |
| 926 | .setIcon( UNDO ) | |
| 927 | .setAction( e -> getActiveEditorPane().undo() ) | |
| 928 | .setDisable( createActiveBooleanProperty( | |
| 929 | FileEditorTab::canUndoProperty ).not() ) | |
| 930 | .build(); | |
| 931 | final Action editRedoAction = new ActionBuilder() | |
| 932 | .setText( "Main.menu.edit.redo" ) | |
| 933 | .setAccelerator( "Shortcut+Y" ) | |
| 934 | .setIcon( REPEAT ) | |
| 935 | .setAction( e -> getActiveEditorPane().redo() ) | |
| 936 | .setDisable( createActiveBooleanProperty( | |
| 937 | FileEditorTab::canRedoProperty ).not() ) | |
| 938 | .build(); | |
| 939 | ||
| 940 | final Action editCutAction = new ActionBuilder() | |
| 941 | .setText( "Main.menu.edit.cut" ) | |
| 942 | .setAccelerator( "Shortcut+X" ) | |
| 943 | .setIcon( CUT ) | |
| 944 | .setAction( e -> getActiveEditorPane().cut() ) | |
| 945 | .setDisable( activeFileEditorIsNull ) | |
| 946 | .build(); | |
| 947 | final Action editCopyAction = new ActionBuilder() | |
| 948 | .setText( "Main.menu.edit.copy" ) | |
| 949 | .setAccelerator( "Shortcut+C" ) | |
| 950 | .setIcon( COPY ) | |
| 951 | .setAction( e -> getActiveEditorPane().copy() ) | |
| 952 | .setDisable( activeFileEditorIsNull ) | |
| 953 | .build(); | |
| 954 | final Action editPasteAction = new ActionBuilder() | |
| 955 | .setText( "Main.menu.edit.paste" ) | |
| 956 | .setAccelerator( "Shortcut+V" ) | |
| 957 | .setIcon( PASTE ) | |
| 958 | .setAction( e -> getActiveEditorPane().paste() ) | |
| 959 | .setDisable( activeFileEditorIsNull ) | |
| 960 | .build(); | |
| 961 | final Action editSelectAllAction = new ActionBuilder() | |
| 962 | .setText( "Main.menu.edit.selectAll" ) | |
| 963 | .setAccelerator( "Shortcut+A" ) | |
| 964 | .setAction( e -> getActiveEditorPane().selectAll() ) | |
| 965 | .setDisable( activeFileEditorIsNull ) | |
| 966 | .build(); | |
| 967 | ||
| 968 | final Action editFindAction = new ActionBuilder() | |
| 969 | .setText( "Main.menu.edit.find" ) | |
| 970 | .setAccelerator( "Ctrl+F" ) | |
| 971 | .setIcon( SEARCH ) | |
| 972 | .setAction( e -> editFind() ) | |
| 973 | .setDisable( activeFileEditorIsNull ) | |
| 974 | .build(); | |
| 975 | final Action editFindNextAction = new ActionBuilder() | |
| 976 | .setText( "Main.menu.edit.find.next" ) | |
| 977 | .setAccelerator( "F3" ) | |
| 978 | .setIcon( null ) | |
| 979 | .setAction( e -> editFindNext() ) | |
| 980 | .setDisable( activeFileEditorIsNull ) | |
| 981 | .build(); | |
| 982 | final Action editPreferencesAction = new ActionBuilder() | |
| 983 | .setText( "Main.menu.edit.preferences" ) | |
| 984 | .setAccelerator( "Ctrl+Alt+S" ) | |
| 985 | .setAction( e -> editPreferences() ) | |
| 986 | .build(); | |
| 987 | ||
| 988 | // Format actions | |
| 989 | final Action formatBoldAction = new ActionBuilder() | |
| 990 | .setText( "Main.menu.format.bold" ) | |
| 991 | .setAccelerator( "Shortcut+B" ) | |
| 992 | .setIcon( BOLD ) | |
| 993 | .setAction( e -> insertMarkdown( "**", "**" ) ) | |
| 994 | .setDisable( activeFileEditorIsNull ) | |
| 995 | .build(); | |
| 996 | final Action formatItalicAction = new ActionBuilder() | |
| 997 | .setText( "Main.menu.format.italic" ) | |
| 998 | .setAccelerator( "Shortcut+I" ) | |
| 999 | .setIcon( ITALIC ) | |
| 1000 | .setAction( e -> insertMarkdown( "*", "*" ) ) | |
| 1001 | .setDisable( activeFileEditorIsNull ) | |
| 1002 | .build(); | |
| 1003 | final Action formatSuperscriptAction = new ActionBuilder() | |
| 1004 | .setText( "Main.menu.format.superscript" ) | |
| 1005 | .setAccelerator( "Shortcut+[" ) | |
| 1006 | .setIcon( SUPERSCRIPT ) | |
| 1007 | .setAction( e -> insertMarkdown( "^", "^" ) ) | |
| 1008 | .setDisable( activeFileEditorIsNull ) | |
| 1009 | .build(); | |
| 1010 | final Action formatSubscriptAction = new ActionBuilder() | |
| 1011 | .setText( "Main.menu.format.subscript" ) | |
| 1012 | .setAccelerator( "Shortcut+]" ) | |
| 1013 | .setIcon( SUBSCRIPT ) | |
| 1014 | .setAction( e -> insertMarkdown( "~", "~" ) ) | |
| 1015 | .setDisable( activeFileEditorIsNull ) | |
| 1016 | .build(); | |
| 1017 | final Action formatStrikethroughAction = new ActionBuilder() | |
| 1018 | .setText( "Main.menu.format.strikethrough" ) | |
| 1019 | .setAccelerator( "Shortcut+T" ) | |
| 1020 | .setIcon( STRIKETHROUGH ) | |
| 1021 | .setAction( e -> insertMarkdown( "~~", "~~" ) ) | |
| 1022 | .setDisable( activeFileEditorIsNull ) | |
| 1023 | .build(); | |
| 1024 | ||
| 1025 | // Insert actions | |
| 1026 | final Action insertBlockquoteAction = new ActionBuilder() | |
| 1027 | .setText( "Main.menu.insert.blockquote" ) | |
| 1028 | .setAccelerator( "Ctrl+Q" ) | |
| 1029 | .setIcon( QUOTE_LEFT ) | |
| 1030 | .setAction( e -> insertMarkdown( "\n\n> ", "" ) ) | |
| 1031 | .setDisable( activeFileEditorIsNull ) | |
| 1032 | .build(); | |
| 1033 | final Action insertCodeAction = new ActionBuilder() | |
| 1034 | .setText( "Main.menu.insert.code" ) | |
| 1035 | .setAccelerator( "Shortcut+K" ) | |
| 1036 | .setIcon( CODE ) | |
| 1037 | .setAction( e -> insertMarkdown( "`", "`" ) ) | |
| 1038 | .setDisable( activeFileEditorIsNull ) | |
| 1039 | .build(); | |
| 1040 | final Action insertFencedCodeBlockAction = new ActionBuilder() | |
| 1041 | .setText( "Main.menu.insert.fenced_code_block" ) | |
| 1042 | .setAccelerator( "Shortcut+Shift+K" ) | |
| 1043 | .setIcon( FILE_CODE_ALT ) | |
| 1044 | .setAction( e -> insertMarkdown( | |
| 1045 | "\n\n```\n", | |
| 1046 | "\n```\n\n", | |
| 1047 | get( "Main.menu.insert.fenced_code_block.prompt" ) ) ) | |
| 1048 | .setDisable( activeFileEditorIsNull ) | |
| 1049 | .build(); | |
| 1050 | final Action insertLinkAction = new ActionBuilder() | |
| 1051 | .setText( "Main.menu.insert.link" ) | |
| 1052 | .setAccelerator( "Shortcut+L" ) | |
| 1053 | .setIcon( LINK ) | |
| 1054 | .setAction( e -> getActiveEditorPane().insertLink() ) | |
| 1055 | .setDisable( activeFileEditorIsNull ) | |
| 1056 | .build(); | |
| 1057 | final Action insertImageAction = new ActionBuilder() | |
| 1058 | .setText( "Main.menu.insert.image" ) | |
| 1059 | .setAccelerator( "Shortcut+G" ) | |
| 1060 | .setIcon( PICTURE_ALT ) | |
| 1061 | .setAction( e -> getActiveEditorPane().insertImage() ) | |
| 1062 | .setDisable( activeFileEditorIsNull ) | |
| 1063 | .build(); | |
| 1064 | ||
| 1065 | // Number of heading actions (H1 ... H3) | |
| 1066 | final int HEADINGS = 3; | |
| 1067 | final Action[] headings = new Action[ HEADINGS ]; | |
| 1068 | ||
| 1069 | for( int i = 1; i <= HEADINGS; i++ ) { | |
| 1070 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 1071 | final String markup = String.format( "%n%n%s ", hashes ); | |
| 1072 | final String text = "Main.menu.insert.heading." + i; | |
| 1073 | final String accelerator = "Shortcut+" + i; | |
| 1074 | final String prompt = text + ".prompt"; | |
| 1075 | ||
| 1076 | headings[ i - 1 ] = new ActionBuilder() | |
| 1077 | .setText( text ) | |
| 1078 | .setAccelerator( accelerator ) | |
| 1079 | .setIcon( HEADER ) | |
| 1080 | .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) ) | |
| 1081 | .setDisable( activeFileEditorIsNull ) | |
| 1082 | .build(); | |
| 1083 | } | |
| 1084 | ||
| 1085 | final Action insertUnorderedListAction = new ActionBuilder() | |
| 1086 | .setText( "Main.menu.insert.unordered_list" ) | |
| 1087 | .setAccelerator( "Shortcut+U" ) | |
| 1088 | .setIcon( LIST_UL ) | |
| 1089 | .setAction( e -> insertMarkdown( "\n\n* ", "" ) ) | |
| 1090 | .setDisable( activeFileEditorIsNull ) | |
| 1091 | .build(); | |
| 1092 | final Action insertOrderedListAction = new ActionBuilder() | |
| 1093 | .setText( "Main.menu.insert.ordered_list" ) | |
| 1094 | .setAccelerator( "Shortcut+Shift+O" ) | |
| 1095 | .setIcon( LIST_OL ) | |
| 1096 | .setAction( e -> insertMarkdown( | |
| 1097 | "\n\n1. ", "" ) ) | |
| 1098 | .setDisable( activeFileEditorIsNull ) | |
| 1099 | .build(); | |
| 1100 | final Action insertHorizontalRuleAction = new ActionBuilder() | |
| 1101 | .setText( "Main.menu.insert.horizontal_rule" ) | |
| 1102 | .setAccelerator( "Shortcut+H" ) | |
| 1103 | .setAction( e -> insertMarkdown( | |
| 1104 | "\n\n---\n\n", "" ) ) | |
| 1105 | .setDisable( activeFileEditorIsNull ) | |
| 1106 | .build(); | |
| 1107 | ||
| 1108 | // Definition actions | |
| 1109 | final Action definitionCreateAction = new ActionBuilder() | |
| 1110 | .setText( "Main.menu.definition.create" ) | |
| 1111 | .setIcon( TREE ) | |
| 1112 | .setAction( e -> getDefinitionPane().addItem() ) | |
| 1113 | .build(); | |
| 1114 | final Action definitionInsertAction = new ActionBuilder() | |
| 1115 | .setText( "Main.menu.definition.insert" ) | |
| 1116 | .setAccelerator( "Ctrl+Space" ) | |
| 1117 | .setIcon( STAR ) | |
| 1118 | .setAction( e -> definitionInsert() ) | |
| 1119 | .build(); | |
| 1120 | ||
| 1121 | // Help actions | |
| 1122 | final Action helpAboutAction = new ActionBuilder() | |
| 1123 | .setText( "Main.menu.help.about" ) | |
| 1124 | .setAction( e -> helpAbout() ) | |
| 1125 | .build(); | |
| 1126 | ||
| 1127 | //---- MenuBar ---- | |
| 1128 | ||
| 1129 | // File Menu | |
| 1130 | final var fileMenu = ActionUtils.createMenu( | |
| 1131 | get( "Main.menu.file" ), | |
| 1132 | fileNewAction, | |
| 1133 | fileOpenAction, | |
| 1134 | null, | |
| 1135 | fileCloseAction, | |
| 1136 | fileCloseAllAction, | |
| 1137 | null, | |
| 1138 | fileSaveAction, | |
| 1139 | fileSaveAsAction, | |
| 1140 | fileSaveAllAction, | |
| 1141 | null, | |
| 1142 | fileExitAction ); | |
| 1143 | ||
| 1144 | // Edit Menu | |
| 1145 | final var editMenu = ActionUtils.createMenu( | |
| 1146 | get( "Main.menu.edit" ), | |
| 1147 | editCopyHtmlAction, | |
| 1148 | null, | |
| 1149 | editUndoAction, | |
| 1150 | editRedoAction, | |
| 1151 | null, | |
| 1152 | editCutAction, | |
| 1153 | editCopyAction, | |
| 1154 | editPasteAction, | |
| 1155 | editSelectAllAction, | |
| 1156 | null, | |
| 1157 | editFindAction, | |
| 1158 | editFindNextAction, | |
| 1159 | null, | |
| 1160 | editPreferencesAction ); | |
| 1161 | ||
| 1162 | // Format Menu | |
| 1163 | final var formatMenu = ActionUtils.createMenu( | |
| 1164 | get( "Main.menu.format" ), | |
| 1165 | formatBoldAction, | |
| 1166 | formatItalicAction, | |
| 1167 | formatSuperscriptAction, | |
| 1168 | formatSubscriptAction, | |
| 1169 | formatStrikethroughAction | |
| 1170 | ); | |
| 1171 | ||
| 1172 | // Insert Menu | |
| 1173 | final var insertMenu = ActionUtils.createMenu( | |
| 1174 | get( "Main.menu.insert" ), | |
| 1175 | insertBlockquoteAction, | |
| 1176 | insertCodeAction, | |
| 1177 | insertFencedCodeBlockAction, | |
| 1178 | null, | |
| 1179 | insertLinkAction, | |
| 1180 | insertImageAction, | |
| 1181 | null, | |
| 1182 | headings[ 0 ], | |
| 1183 | headings[ 1 ], | |
| 1184 | headings[ 2 ], | |
| 1185 | null, | |
| 1186 | insertUnorderedListAction, | |
| 1187 | insertOrderedListAction, | |
| 1188 | insertHorizontalRuleAction | |
| 1189 | ); | |
| 1190 | ||
| 1191 | // Definition Menu | |
| 1192 | final var definitionMenu = ActionUtils.createMenu( | |
| 1193 | get( "Main.menu.definition" ), | |
| 1194 | definitionCreateAction, | |
| 1195 | definitionInsertAction ); | |
| 1196 | ||
| 1197 | // Help Menu | |
| 1198 | final var helpMenu = ActionUtils.createMenu( | |
| 1199 | get( "Main.menu.help" ), | |
| 1200 | helpAboutAction ); | |
| 1201 | ||
| 1202 | //---- MenuBar ---- | |
| 1203 | final var menuBar = new MenuBar( | |
| 1204 | fileMenu, | |
| 1205 | editMenu, | |
| 1206 | formatMenu, | |
| 1207 | insertMenu, | |
| 1208 | definitionMenu, | |
| 1209 | helpMenu ); | |
| 1210 | ||
| 1211 | //---- ToolBar ---- | |
| 1212 | final var toolBar = ActionUtils.createToolBar( | |
| 1213 | fileNewAction, | |
| 1214 | fileOpenAction, | |
| 1215 | fileSaveAction, | |
| 1216 | null, | |
| 1217 | editUndoAction, | |
| 1218 | editRedoAction, | |
| 1219 | editCutAction, | |
| 1220 | editCopyAction, | |
| 1221 | editPasteAction, | |
| 1222 | null, | |
| 1223 | formatBoldAction, | |
| 1224 | formatItalicAction, | |
| 1225 | formatSuperscriptAction, | |
| 1226 | formatSubscriptAction, | |
| 1227 | insertBlockquoteAction, | |
| 1228 | insertCodeAction, | |
| 1229 | insertFencedCodeBlockAction, | |
| 1230 | null, | |
| 1231 | insertLinkAction, | |
| 1232 | insertImageAction, | |
| 1233 | null, | |
| 1234 | headings[ 0 ], | |
| 1235 | null, | |
| 1236 | insertUnorderedListAction, | |
| 1237 | insertOrderedListAction ); | |
| 1238 | ||
| 1239 | return new VBox( menuBar, toolBar ); | |
| 1240 | } | |
| 1241 | ||
| 1242 | /** | |
| 1243 | * Performs the autoinsert function on the active file editor. | |
| 1244 | */ | |
| 1245 | private void definitionInsert() { | |
| 1246 | getDefinitionNameInjector().autoinsert(); | |
| 1247 | } | |
| 1248 | ||
| 1249 | /** | |
| 1250 | * Creates a boolean property that is bound to another boolean value of the | |
| 1251 | * active editor. | |
| 1252 | */ | |
| 1253 | private BooleanProperty createActiveBooleanProperty( | |
| 1254 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 1255 | ||
| 1256 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 1257 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 1258 | ||
| 1259 | if( tab != null ) { | |
| 1260 | b.bind( func.apply( tab ) ); | |
| 1261 | } | |
| 1262 | ||
| 1263 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 1264 | ( observable, oldFileEditor, newFileEditor ) -> { | |
| 1265 | b.unbind(); | |
| 1266 | ||
| 1267 | if( newFileEditor == null ) { | |
| 1268 | b.set( false ); | |
| 1269 | } | |
| 1270 | else { | |
| 1271 | b.bind( func.apply( newFileEditor ) ); | |
| 1272 | } | |
| 1273 | } | |
| 1274 | ); | |
| 1275 | ||
| 1276 | return b; | |
| 1277 | } | |
| 1278 | ||
| 1279 | //---- Convenience accessors ---------------------------------------------- | |
| 1280 | ||
| 1281 | private Preferences getPreferences() { | |
| 1282 | return sOptions.getState(); | |
| 1283 | } | |
| 1284 | ||
| 1285 | private int getCurrentParagraphIndex() { | |
| 1286 | return getActiveEditorPane().getCurrentParagraphIndex(); | |
| 1287 | } | |
| 1288 | ||
| 1289 | private float getFloat( final String key, final float defaultValue ) { | |
| 1290 | return getPreferences().getFloat( key, defaultValue ); | |
| 1291 | } | |
| 1292 | ||
| 1293 | public Window getWindow() { | |
| 1294 | return getScene().getWindow(); | |
| 1295 | } | |
| 1296 | ||
| 1297 | private MarkdownEditorPane getActiveEditorPane() { | |
| 1298 | return getActiveFileEditorTab().getEditorPane(); | |
| 1299 | } | |
| 1300 | ||
| 1301 | private FileEditorTab getActiveFileEditorTab() { | |
| 1302 | return getFileEditorPane().getActiveFileEditor(); | |
| 1303 | } | |
| 1304 | ||
| 1305 | //---- Member accessors --------------------------------------------------- | |
| 1306 | ||
| 1307 | protected Scene getScene() { | |
| 1308 | return mScene; | |
| 1309 | } | |
| 1310 | ||
| 1311 | private SpellChecker getSpellChecker() { | |
| 1312 | return mSpellChecker; | |
| 1313 | } | |
| 1314 | ||
| 1315 | private Map<FileEditorTab, Processor<String>> getProcessors() { | |
| 1316 | return mProcessors; | |
| 1317 | } | |
| 1318 | ||
| 1319 | private FileEditorTabPane getFileEditorPane() { | |
| 1320 | return mFileEditorPane; | |
| 1321 | } | |
| 1322 | ||
| 1323 | private HTMLPreviewPane getPreviewPane() { | |
| 1324 | return mPreviewPane; | |
| 1325 | } | |
| 1326 | ||
| 1327 | private void setDefinitionSource( | |
| 1328 | final DefinitionSource definitionSource ) { | |
| 1329 | assert definitionSource != null; | |
| 1330 | mDefinitionSource = definitionSource; | |
| 1331 | } | |
| 1332 | ||
| 1333 | private DefinitionSource getDefinitionSource() { | |
| 1334 | return mDefinitionSource; | |
| 1335 | } | |
| 1336 | ||
| 1337 | private DefinitionPane getDefinitionPane() { | |
| 1338 | return mDefinitionPane; | |
| 1339 | } | |
| 1340 | ||
| 1341 | private Text getLineNumberText() { | |
| 1342 | return mLineNumberText; | |
| 1343 | } | |
| 1344 | ||
| 1345 | private StatusBar getStatusBar() { | |
| 1346 | return mStatusBar; | |
| 1347 | } | |
| 1348 | ||
| 1349 | private TextField getFindTextField() { | |
| 1350 | return mFindTextField; | |
| 1351 | } | |
| 1352 | ||
| 1353 | private DefinitionNameInjector getDefinitionNameInjector() { | |
| 1354 | return mDefinitionNameInjector; | |
| 1355 | } | |
| 1356 | ||
| 1357 | /** | |
| 1358 | * Returns the variable map of interpolated definitions. | |
| 1359 | * | |
| 1360 | * @return A map to help dereference variables. | |
| 1361 | */ | |
| 1362 | private Map<String, String> getResolvedMap() { | |
| 1363 | return mResolvedMap; | |
| 1364 | } | |
| 1365 | ||
| 1366 | //---- Persistence accessors ---------------------------------------------- | |
| 1367 | ||
| 1368 | private UserPreferences getUserPreferences() { | |
| 1369 | return UserPreferences.getInstance(); | |
| 1370 | } | |
| 1371 | ||
| 1372 | private Path getDefinitionPath() { | |
| 1373 | return getUserPreferences().getDefinitionPath(); | |
| 1374 | } | |
| 1375 | ||
| 1376 | //---- Spelling ----------------------------------------------------------- | |
| 1377 | ||
| 1378 | /** | |
| 1379 | * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}. | |
| 1380 | * This is called to spell check the document, rather than a single paragraph. | |
| 1381 | * | |
| 1382 | * @param text The full document text. | |
| 1383 | */ | |
| 1384 | private void spellcheck( | |
| 1385 | final StyleClassedTextArea editor, final String text ) { | |
| 1386 | spellcheck( editor, text, -1 ); | |
| 1387 | } | |
| 1388 | ||
| 1389 | /** | |
| 1390 | * Spellchecks a subset of the entire document. | |
| 1391 | * | |
| 1392 | * @param text Look up words for this text in the lexicon. | |
| 1393 | * @param paraId Set to -1 to apply resulting style spans to the entire | |
| 1394 | * text. | |
| 1395 | */ | |
| 1396 | private void spellcheck( | |
| 1397 | final StyleClassedTextArea editor, final String text, final int paraId ) { | |
| 1398 | final var builder = new StyleSpansBuilder<Collection<String>>(); | |
| 1399 | final var runningIndex = new AtomicInteger( 0 ); | |
| 1400 | final var checker = getSpellChecker(); | |
| 1401 | ||
| 1402 | // The text nodes must be relayed through a contextual "visitor" that | |
| 1403 | // can return text in chunks with correlative offsets into the string. | |
| 1404 | // This allows Markdown, R Markdown, XML, and R XML documents to return | |
| 1405 | // sets of words to check. | |
| 1406 | ||
| 1407 | final var node = mParser.parse( text ); | |
| 1408 | final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> { | |
| 1409 | // Treat hyphenated compound words as individual words. | |
| 1410 | final var check = visited.replace( '-', ' ' ); | |
| 1411 | ||
| 1412 | checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> { | |
| 1413 | prevIndex += bIndex; | |
| 1414 | currIndex += bIndex; | |
| 1415 | ||
| 1416 | // Clear styling between lexiconically absent words. | |
| 1417 | builder.add( emptyList(), prevIndex - runningIndex.get() ); | |
| 1418 | builder.add( singleton( "spelling" ), currIndex - prevIndex ); | |
| 1419 | runningIndex.set( currIndex ); | |
| 1420 | } ); | |
| 1421 | } ); | |
| 1422 | ||
| 1423 | visitor.visit( node ); | |
| 1424 | ||
| 1425 | // If the running index was set, at least one word triggered the listener. | |
| 1426 | if( runningIndex.get() > 0 ) { | |
| 1427 | // Clear styling after the last lexiconically absent word. | |
| 1428 | builder.add( emptyList(), text.length() - runningIndex.get() ); | |
| 1429 | ||
| 1430 | final var spans = builder.create(); | |
| 1431 | ||
| 1432 | if( paraId >= 0 ) { | |
| 1433 | editor.setStyleSpans( paraId, 0, spans ); | |
| 1434 | } | |
| 1435 | else { | |
| 1436 | editor.setStyleSpans( 0, spans ); | |
| 1437 | } | |
| 1438 | } | |
| 1439 | } | |
| 1440 | ||
| 1441 | @SuppressWarnings("SameParameterValue") | |
| 1442 | private Collection<String> readLexicon( final String filename ) | |
| 1443 | throws Exception { | |
| 1444 | final var path = "/" + LEXICONS_DIRECTORY + "/" + filename; | |
| 1445 | ||
| 1446 | try( final var resource = getClass().getResourceAsStream( path ) ) { | |
| 1447 | if( resource == null ) { | |
| 1448 | throw new FileNotFoundException( path ); | |
| 1449 | } | |
| 1450 | ||
| 1451 | try( final var isr = new InputStreamReader( resource, UTF_8 ); | |
| 1452 | final var reader = new BufferedReader( isr ) ) { | |
| 1453 | return reader.lines().collect( Collectors.toList() ); | |
| 1454 | } | |
| 1455 | } | |
| 1456 | } | |
| 1457 | ||
| 1458 | // TODO: Replace using Markdown processor instantiated for Markdown files. | |
| 1459 | // FIXME: https://github.com/DaveJarvis/scrivenvar/issues/59 | |
| 1460 | private final Parser mParser = Parser.builder().build(); | |
| 1461 | ||
| 1462 | // TODO: Replace with generic interface; provide Markdown/XML implementations. | |
| 1463 | // FIXME: https://github.com/DaveJarvis/scrivenvar/issues/59 | |
| 1464 | private static final class TextVisitor { | |
| 1465 | private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>( | |
| 1466 | com.vladsch.flexmark.ast.Text.class, this::visit ) | |
| 1467 | ); | |
| 1468 | ||
| 1469 | private final SpellCheckListener mConsumer; | |
| 1470 | ||
| 1471 | public TextVisitor( final SpellCheckListener consumer ) { | |
| 1472 | mConsumer = consumer; | |
| 1473 | } | |
| 1474 | ||
| 1475 | private void visit( final com.vladsch.flexmark.util.ast.Node node ) { | |
| 1476 | if( node instanceof com.vladsch.flexmark.ast.Text ) { | |
| 1477 | mConsumer.accept( node.getChars().toString(), | |
| 1478 | node.getStartOffset(), | |
| 1479 | node.getEndOffset() ); | |
| 1480 | } | |
| 1481 | ||
| 1482 | mVisitor.visitChildren( node ); | |
| 1483 | } | |
| 1484 | } | |
| 1485 | } | |
| 1486 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * * Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * * Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.scrivenvar; | |
| 28 | ||
| 29 | import java.text.MessageFormat; | |
| 30 | import java.util.ResourceBundle; | |
| 31 | import java.util.Stack; | |
| 32 | ||
| 33 | import static com.scrivenvar.Constants.APP_BUNDLE_NAME; | |
| 34 | import static java.util.ResourceBundle.getBundle; | |
| 35 | ||
| 36 | /** | |
| 37 | * Recursively resolves message properties. Property values can refer to other | |
| 38 | * properties using a <code>${var}</code> syntax. | |
| 39 | */ | |
| 40 | public class Messages { | |
| 41 | ||
| 42 | private static final ResourceBundle RESOURCE_BUNDLE = | |
| 43 | getBundle( APP_BUNDLE_NAME ); | |
| 44 | ||
| 45 | private Messages() { | |
| 46 | } | |
| 47 | ||
| 48 | /** | |
| 49 | * Return the value of a resource bundle value after having resolved any | |
| 50 | * references to other bundle variables. | |
| 51 | * | |
| 52 | * @param props The bundle containing resolvable properties. | |
| 53 | * @param s The value for a key to resolve. | |
| 54 | * @return The value of the key with all references recursively dereferenced. | |
| 55 | */ | |
| 56 | @SuppressWarnings("SameParameterValue") | |
| 57 | private static String resolve( final ResourceBundle props, final String s ) { | |
| 58 | final int len = s.length(); | |
| 59 | final Stack<StringBuilder> stack = new Stack<>(); | |
| 60 | ||
| 61 | StringBuilder sb = new StringBuilder( 256 ); | |
| 62 | boolean open = false; | |
| 63 | ||
| 64 | for( int i = 0; i < len; i++ ) { | |
| 65 | final char c = s.charAt( i ); | |
| 66 | ||
| 67 | switch( c ) { | |
| 68 | case '$': { | |
| 69 | if( i + 1 < len && s.charAt( i + 1 ) == '{' ) { | |
| 70 | stack.push( sb ); | |
| 71 | sb = new StringBuilder( 256 ); | |
| 72 | i++; | |
| 73 | open = true; | |
| 74 | } | |
| 75 | ||
| 76 | break; | |
| 77 | } | |
| 78 | ||
| 79 | case '}': { | |
| 80 | if( open ) { | |
| 81 | open = false; | |
| 82 | final String name = sb.toString(); | |
| 83 | ||
| 84 | sb = stack.pop(); | |
| 85 | sb.append( props.getString( name ) ); | |
| 86 | break; | |
| 87 | } | |
| 88 | } | |
| 89 | ||
| 90 | default: { | |
| 91 | sb.append( c ); | |
| 92 | break; | |
| 93 | } | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | if( open ) { | |
| 98 | throw new IllegalArgumentException( "missing '}'" ); | |
| 99 | } | |
| 100 | ||
| 101 | return sb.toString(); | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Returns the value for a key from the message bundle. | |
| 106 | * | |
| 107 | * @param key Retrieve the value for this key. | |
| 108 | * @return The value for the key. | |
| 109 | */ | |
| 110 | public static String get( final String key ) { | |
| 111 | try { | |
| 112 | return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) ); | |
| 113 | } catch( final Exception ex ) { | |
| 114 | return key; | |
| 115 | } | |
| 116 | } | |
| 117 | ||
| 118 | public static String getLiteral( final String key ) { | |
| 119 | return RESOURCE_BUNDLE.getString( key ); | |
| 120 | } | |
| 121 | ||
| 122 | public static String get( final String key, final boolean interpolate ) { | |
| 123 | return interpolate ? get( key ) : getLiteral( key ); | |
| 124 | } | |
| 125 | ||
| 126 | /** | |
| 127 | * Returns the value for a key from the message bundle with the arguments | |
| 128 | * replacing <code>{#}</code> place holders. | |
| 129 | * | |
| 130 | * @param key Retrieve the value for this key. | |
| 131 | * @param args The values to substitute for place holders. | |
| 132 | * @return The value for the key. | |
| 133 | */ | |
| 134 | public static String get( final String key, final Object... args ) { | |
| 135 | return MessageFormat.format( get( key ), args ); | |
| 136 | } | |
| 137 | } | |
| 138 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import javafx.beans.property.BooleanProperty; | |
| 31 | import javafx.beans.property.SimpleBooleanProperty; | |
| 32 | import javafx.event.Event; | |
| 33 | import javafx.event.EventHandler; | |
| 34 | import javafx.scene.Node; | |
| 35 | import javafx.scene.control.ScrollBar; | |
| 36 | import javafx.scene.control.skin.ScrollBarSkin; | |
| 37 | import javafx.scene.input.MouseEvent; | |
| 38 | import javafx.scene.input.ScrollEvent; | |
| 39 | import javafx.scene.layout.StackPane; | |
| 40 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 41 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 42 | ||
| 43 | import javax.swing.*; | |
| 44 | ||
| 45 | import static javafx.geometry.Orientation.VERTICAL; | |
| 46 | ||
| 47 | /** | |
| 48 | * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to | |
| 49 | * an instance of {@link JScrollBar}. | |
| 50 | * <p> | |
| 51 | * Called to synchronize the scrolling areas for either scrolling with the | |
| 52 | * mouse or scrolling using the scrollbar's thumb. Both are required to avoid | |
| 53 | * scrolling on the estimatedScrollYProperty that occurs when text events | |
| 54 | * fire. Scrolling performed for text events are handled separately to ensure | |
| 55 | * the preview panel scrolls to the same position in the Markdown editor, | |
| 56 | * taking into account things like images, tables, and other potentially | |
| 57 | * long vertical presentation items. | |
| 58 | * </p> | |
| 59 | */ | |
| 60 | public final class ScrollEventHandler implements EventHandler<Event> { | |
| 61 | ||
| 62 | private final class MouseHandler implements EventHandler<MouseEvent> { | |
| 63 | private final EventHandler<? super MouseEvent> mOldHandler; | |
| 64 | ||
| 65 | /** | |
| 66 | * Constructs a new handler for mouse scrolling events. | |
| 67 | * | |
| 68 | * @param oldHandler Receives the event after scrolling takes place. | |
| 69 | */ | |
| 70 | private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) { | |
| 71 | mOldHandler = oldHandler; | |
| 72 | } | |
| 73 | ||
| 74 | @Override | |
| 75 | public void handle( final MouseEvent event ) { | |
| 76 | ScrollEventHandler.this.handle( event ); | |
| 77 | mOldHandler.handle( event ); | |
| 78 | } | |
| 79 | } | |
| 80 | ||
| 81 | private final class ScrollHandler implements EventHandler<ScrollEvent> { | |
| 82 | @Override | |
| 83 | public void handle( final ScrollEvent event ) { | |
| 84 | ScrollEventHandler.this.handle( event ); | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane; | |
| 89 | private final JScrollBar mPreviewScrollBar; | |
| 90 | private final BooleanProperty mEnabled = new SimpleBooleanProperty(); | |
| 91 | ||
| 92 | /** | |
| 93 | * @param editorScrollPane Scroll event source (human movement). | |
| 94 | * @param previewScrollBar Scroll event destination (corresponding movement). | |
| 95 | */ | |
| 96 | public ScrollEventHandler( | |
| 97 | final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane, | |
| 98 | final JScrollBar previewScrollBar ) { | |
| 99 | mEditorScrollPane = editorScrollPane; | |
| 100 | mPreviewScrollBar = previewScrollBar; | |
| 101 | ||
| 102 | mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() ); | |
| 103 | ||
| 104 | final var thumb = getVerticalScrollBarThumb( mEditorScrollPane ); | |
| 105 | thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) ); | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * Gets a property intended to be bound to selected property of the tab being | |
| 110 | * scrolled. This is required because there's only one preview pane but | |
| 111 | * multiple editor panes. Each editor pane maintains its own scroll position. | |
| 112 | * | |
| 113 | * @return A {@link BooleanProperty} representing whether the scroll | |
| 114 | * events for this tab are to be executed. | |
| 115 | */ | |
| 116 | public BooleanProperty enabledProperty() { | |
| 117 | return mEnabled; | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm | |
| 122 | * is based on Karl Tauber's ratio calculation. | |
| 123 | * | |
| 124 | * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent} | |
| 125 | */ | |
| 126 | @Override | |
| 127 | public void handle( final Event event ) { | |
| 128 | if( isEnabled() ) { | |
| 129 | final var eScrollPane = getEditorScrollPane(); | |
| 130 | final int eScrollY = | |
| 131 | eScrollPane.estimatedScrollYProperty().getValue().intValue(); | |
| 132 | final int eHeight = (int) | |
| 133 | (eScrollPane.totalHeightEstimateProperty().getValue().intValue() | |
| 134 | - eScrollPane.getHeight()); | |
| 135 | final double eRatio = eHeight > 0 | |
| 136 | ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0; | |
| 137 | ||
| 138 | final var pScrollBar = getPreviewScrollBar(); | |
| 139 | final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight(); | |
| 140 | final var pScrollY = (int) (pHeight * eRatio); | |
| 141 | ||
| 142 | pScrollBar.setValue( pScrollY ); | |
| 143 | pScrollBar.getParent().repaint(); | |
| 144 | } | |
| 145 | } | |
| 146 | ||
| 147 | private StackPane getVerticalScrollBarThumb( | |
| 148 | final VirtualizedScrollPane<StyleClassedTextArea> pane ) { | |
| 149 | final ScrollBar scrollBar = getVerticalScrollBar( pane ); | |
| 150 | final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get()); | |
| 151 | ||
| 152 | for( final Node node : skin.getChildren() ) { | |
| 153 | // Brittle, but what can you do? | |
| 154 | if( node.getStyleClass().contains( "thumb" ) ) { | |
| 155 | return (StackPane) node; | |
| 156 | } | |
| 157 | } | |
| 158 | ||
| 159 | throw new IllegalArgumentException( "No scroll bar skin found." ); | |
| 160 | } | |
| 161 | ||
| 162 | private ScrollBar getVerticalScrollBar( | |
| 163 | final VirtualizedScrollPane<StyleClassedTextArea> pane ) { | |
| 164 | ||
| 165 | for( final Node node : pane.getChildrenUnmodifiable() ) { | |
| 166 | if( node instanceof ScrollBar ) { | |
| 167 | final ScrollBar scrollBar = (ScrollBar) node; | |
| 168 | ||
| 169 | if( scrollBar.getOrientation() == VERTICAL ) { | |
| 170 | return scrollBar; | |
| 171 | } | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | throw new IllegalArgumentException( "No vertical scroll pane found." ); | |
| 176 | } | |
| 177 | ||
| 178 | private boolean isEnabled() { | |
| 179 | // TODO: As a minor optimization, when this is set to false, it could remove | |
| 180 | // the MouseHandler and ScrollHandler so that events only dispatch to one | |
| 181 | // object (instead of one per editor tab). | |
| 182 | return mEnabled.get(); | |
| 183 | } | |
| 184 | ||
| 185 | private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() { | |
| 186 | return mEditorScrollPane; | |
| 187 | } | |
| 188 | ||
| 189 | private JScrollBar getPreviewScrollBar() { | |
| 190 | return mPreviewScrollBar; | |
| 191 | } | |
| 192 | } | |
| 193 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import java.util.HashMap; | |
| 31 | import java.util.Map; | |
| 32 | import java.util.ServiceLoader; | |
| 33 | ||
| 34 | /** | |
| 35 | * Responsible for loading services. The services are treated as singleton | |
| 36 | * instances. | |
| 37 | */ | |
| 38 | public class Services { | |
| 39 | ||
| 40 | @SuppressWarnings("rawtypes") | |
| 41 | private static final Map<Class, Object> SINGLETONS = new HashMap<>(); | |
| 42 | ||
| 43 | /** | |
| 44 | * Loads a service based on its interface definition. This will return an | |
| 45 | * existing instance if the class has already been instantiated. | |
| 46 | * | |
| 47 | * @param <T> The service to load. | |
| 48 | * @param api The interface definition for the service. | |
| 49 | * @return A class that implements the interface. | |
| 50 | */ | |
| 51 | @SuppressWarnings("unchecked") | |
| 52 | public static <T> T load( final Class<T> api ) { | |
| 53 | final T o = (T) get( api ); | |
| 54 | ||
| 55 | return o == null ? newInstance( api ) : o; | |
| 56 | } | |
| 57 | ||
| 58 | private static <T> T newInstance( final Class<T> api ) { | |
| 59 | final ServiceLoader<T> services = ServiceLoader.load( api ); | |
| 60 | ||
| 61 | for( final T service : services ) { | |
| 62 | if( service != null ) { | |
| 63 | // Re-use the same instance the next time the class is loaded. | |
| 64 | put( api, service ); | |
| 65 | return service; | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | throw new RuntimeException( "No implementation for: " + api ); | |
| 70 | } | |
| 71 | ||
| 72 | @SuppressWarnings("rawtypes") | |
| 73 | private static void put( final Class key, Object value ) { | |
| 74 | SINGLETONS.put( key, value ); | |
| 75 | } | |
| 76 | ||
| 77 | @SuppressWarnings("rawtypes") | |
| 78 | private static Object get( final Class api ) { | |
| 79 | return SINGLETONS.get( api ); | |
| 80 | } | |
| 81 | } | |
| 82 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import com.scrivenvar.service.events.Notifier; | |
| 31 | import org.controlsfx.control.StatusBar; | |
| 32 | ||
| 33 | import static com.scrivenvar.Constants.STATUS_BAR_OK; | |
| 34 | import static com.scrivenvar.Messages.get; | |
| 35 | import static javafx.application.Platform.runLater; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for passing notifications about exceptions (or other error | |
| 39 | * messages) through the application. Once the Event Bus is implemented, this | |
| 40 | * class can go away. | |
| 41 | */ | |
| 42 | public class StatusBarNotifier { | |
| 43 | private static final String OK = get( STATUS_BAR_OK, "OK" ); | |
| 44 | ||
| 45 | private static final Notifier sNotifier = Services.load( Notifier.class ); | |
| 46 | private static StatusBar sStatusBar; | |
| 47 | ||
| 48 | public static void setStatusBar( final StatusBar statusBar ) { | |
| 49 | sStatusBar = statusBar; | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Resets the status bar to a default message. | |
| 54 | */ | |
| 55 | public static void clearAlert() { | |
| 56 | // Don't burden the repaint thread if there's no status bar change. | |
| 57 | if( !OK.equals( sStatusBar.getText() ) ) { | |
| 58 | update( OK ); | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Updates the status bar with a custom message. | |
| 64 | * | |
| 65 | * @param key The resource bundle key associated with a message (typically | |
| 66 | * to inform the user about an error). | |
| 67 | */ | |
| 68 | public static void alert( final String key ) { | |
| 69 | update( get( key ) ); | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Updates the status bar with a custom message. | |
| 74 | * | |
| 75 | * @param key The property key having a value to populate with arguments. | |
| 76 | * @param args The placeholder values to substitute into the key's value. | |
| 77 | */ | |
| 78 | public static void alert( final String key, final Object... args ) { | |
| 79 | update( get( key, args ) ); | |
| 80 | } | |
| 81 | ||
| 82 | /** | |
| 83 | * Called when an exception occurs that warrants the user's attention. | |
| 84 | * | |
| 85 | * @param t The exception with a message that the user should know about. | |
| 86 | */ | |
| 87 | public static void alert( final Throwable t ) { | |
| 88 | update( t.getMessage() ); | |
| 89 | } | |
| 90 | ||
| 91 | /** | |
| 92 | * Updates the status bar to show the first line of the given message. | |
| 93 | * | |
| 94 | * @param message The message to show in the status bar. | |
| 95 | */ | |
| 96 | private static void update( final String message ) { | |
| 97 | runLater( | |
| 98 | () -> { | |
| 99 | final var s = message == null ? "" : message; | |
| 100 | final var i = s.indexOf( '\n' ); | |
| 101 | sStatusBar.setText( s.substring( 0, i > 0 ? i : s.length() ) ); | |
| 102 | } | |
| 103 | ); | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Returns the global {@link Notifier} instance that can be used for opening | |
| 108 | * pop-up alert messages. | |
| 109 | * | |
| 110 | * @return The pop-up {@link Notifier} dispatcher. | |
| 111 | */ | |
| 112 | public static Notifier getNotifier() { | |
| 113 | return sNotifier; | |
| 114 | } | |
| 115 | } | |
| 116 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.adapters; | |
| 29 | ||
| 30 | import org.xhtmlrenderer.event.DocumentListener; | |
| 31 | ||
| 32 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 33 | ||
| 34 | /** | |
| 35 | * Allows subclasses to implement specific events. | |
| 36 | */ | |
| 37 | public class DocumentAdapter implements DocumentListener { | |
| 38 | @Override | |
| 39 | public void documentStarted() { | |
| 40 | } | |
| 41 | ||
| 42 | @Override | |
| 43 | public void documentLoaded() { | |
| 44 | } | |
| 45 | ||
| 46 | @Override | |
| 47 | public void onLayoutException( final Throwable t ) { | |
| 48 | alert( t ); | |
| 49 | } | |
| 50 | ||
| 51 | @Override | |
| 52 | public void onRenderException( final Throwable t ) { | |
| 53 | alert( t ); | |
| 54 | } | |
| 55 | } | |
| 56 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.adapters; | |
| 29 | ||
| 30 | import org.w3c.dom.Element; | |
| 31 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 32 | import org.xhtmlrenderer.simple.extend.FormSubmissionListener; | |
| 33 | ||
| 34 | public abstract class ReplacedElementAdapter implements ReplacedElementFactory { | |
| 35 | @Override | |
| 36 | public void reset() { | |
| 37 | } | |
| 38 | ||
| 39 | @Override | |
| 40 | public void remove( final Element e ) { | |
| 41 | } | |
| 42 | ||
| 43 | @Override | |
| 44 | public void setFormSubmissionListener( | |
| 45 | final FormSubmissionListener listener ) { | |
| 46 | } | |
| 47 | } | |
| 48 | 1 |
| 1 | /* | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | package com.scrivenvar.controls; | |
| 29 | ||
| 30 | import com.scrivenvar.Messages; | |
| 31 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 32 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 33 | import javafx.beans.property.ObjectProperty; | |
| 34 | import javafx.beans.property.SimpleObjectProperty; | |
| 35 | import javafx.event.ActionEvent; | |
| 36 | import javafx.scene.control.Button; | |
| 37 | import javafx.scene.control.Tooltip; | |
| 38 | import javafx.scene.input.KeyCode; | |
| 39 | import javafx.scene.input.KeyEvent; | |
| 40 | import javafx.stage.FileChooser; | |
| 41 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 42 | ||
| 43 | import java.io.File; | |
| 44 | import java.nio.file.Path; | |
| 45 | import java.util.ArrayList; | |
| 46 | import java.util.List; | |
| 47 | ||
| 48 | /** | |
| 49 | * Button that opens a file chooser to select a local file for a URL. | |
| 50 | */ | |
| 51 | public class BrowseFileButton extends Button { | |
| 52 | private final List<ExtensionFilter> extensionFilters = new ArrayList<>(); | |
| 53 | ||
| 54 | public BrowseFileButton() { | |
| 55 | setGraphic( | |
| 56 | FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT ) | |
| 57 | ); | |
| 58 | setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) ); | |
| 59 | setOnAction( this::browse ); | |
| 60 | ||
| 61 | disableProperty().bind( basePath.isNull() ); | |
| 62 | ||
| 63 | // workaround for a JavaFX bug: | |
| 64 | // avoid closing the dialog that contains this control when the user | |
| 65 | // closes the FileChooser or DirectoryChooser using the ESC key | |
| 66 | addEventHandler( KeyEvent.KEY_RELEASED, e -> { | |
| 67 | if( e.getCode() == KeyCode.ESCAPE ) { | |
| 68 | e.consume(); | |
| 69 | } | |
| 70 | } ); | |
| 71 | } | |
| 72 | ||
| 73 | public void addExtensionFilter( ExtensionFilter extensionFilter ) { | |
| 74 | extensionFilters.add( extensionFilter ); | |
| 75 | } | |
| 76 | ||
| 77 | // 'basePath' property | |
| 78 | private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>(); | |
| 79 | ||
| 80 | public Path getBasePath() { | |
| 81 | return basePath.get(); | |
| 82 | } | |
| 83 | ||
| 84 | public void setBasePath( Path basePath ) { | |
| 85 | this.basePath.set( basePath ); | |
| 86 | } | |
| 87 | ||
| 88 | // 'url' property | |
| 89 | private final ObjectProperty<String> url = new SimpleObjectProperty<>(); | |
| 90 | ||
| 91 | public ObjectProperty<String> urlProperty() { | |
| 92 | return url; | |
| 93 | } | |
| 94 | ||
| 95 | protected void browse( ActionEvent e ) { | |
| 96 | FileChooser fileChooser = new FileChooser(); | |
| 97 | fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) ); | |
| 98 | fileChooser.getExtensionFilters().addAll( extensionFilters ); | |
| 99 | fileChooser.getExtensionFilters() | |
| 100 | .add( new ExtensionFilter( Messages.get( | |
| 101 | "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) ); | |
| 102 | fileChooser.setInitialDirectory( getInitialDirectory() ); | |
| 103 | File result = fileChooser.showOpenDialog( getScene().getWindow() ); | |
| 104 | if( result != null ) { | |
| 105 | updateUrl( result ); | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | protected File getInitialDirectory() { | |
| 110 | //TODO build initial directory based on current value of 'url' property | |
| 111 | return getBasePath().toFile(); | |
| 112 | } | |
| 113 | ||
| 114 | protected void updateUrl( File file ) { | |
| 115 | String newUrl; | |
| 116 | try { | |
| 117 | newUrl = getBasePath().relativize( file.toPath() ).toString(); | |
| 118 | } catch( IllegalArgumentException ex ) { | |
| 119 | newUrl = file.toString(); | |
| 120 | } | |
| 121 | url.set( newUrl.replace( '\\', '/' ) ); | |
| 122 | } | |
| 123 | } | |
| 124 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | package com.scrivenvar.controls; | |
| 29 | ||
| 30 | import javafx.beans.property.SimpleStringProperty; | |
| 31 | import javafx.beans.property.StringProperty; | |
| 32 | import javafx.scene.control.TextField; | |
| 33 | import javafx.util.StringConverter; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for escaping/unescaping characters for markdown. | |
| 37 | */ | |
| 38 | public class EscapeTextField extends TextField { | |
| 39 | ||
| 40 | public EscapeTextField() { | |
| 41 | escapedText.bindBidirectional( | |
| 42 | textProperty(), | |
| 43 | new StringConverter<>() { | |
| 44 | @Override | |
| 45 | public String toString( String object ) { | |
| 46 | return escape( object ); | |
| 47 | } | |
| 48 | ||
| 49 | @Override | |
| 50 | public String fromString( String string ) { | |
| 51 | return unescape( string ); | |
| 52 | } | |
| 53 | } | |
| 54 | ); | |
| 55 | escapeCharacters.addListener( | |
| 56 | e -> escapedText.set( escape( textProperty().get() ) ) | |
| 57 | ); | |
| 58 | } | |
| 59 | ||
| 60 | // 'escapedText' property | |
| 61 | private final StringProperty escapedText = new SimpleStringProperty(); | |
| 62 | ||
| 63 | public StringProperty escapedTextProperty() { | |
| 64 | return escapedText; | |
| 65 | } | |
| 66 | ||
| 67 | // 'escapeCharacters' property | |
| 68 | private final StringProperty escapeCharacters = new SimpleStringProperty(); | |
| 69 | ||
| 70 | public String getEscapeCharacters() { | |
| 71 | return escapeCharacters.get(); | |
| 72 | } | |
| 73 | ||
| 74 | public void setEscapeCharacters( String escapeCharacters ) { | |
| 75 | this.escapeCharacters.set( escapeCharacters ); | |
| 76 | } | |
| 77 | ||
| 78 | private String escape( final String s ) { | |
| 79 | final String escapeChars = getEscapeCharacters(); | |
| 80 | ||
| 81 | return isEmpty( escapeChars ) ? s : | |
| 82 | s.replaceAll( "([" + escapeChars.replaceAll( | |
| 83 | "(.)", | |
| 84 | "\\\\$1" ) + "])", "\\\\$1" ); | |
| 85 | } | |
| 86 | ||
| 87 | private String unescape( final String s ) { | |
| 88 | final String escapeChars = getEscapeCharacters(); | |
| 89 | ||
| 90 | return isEmpty( escapeChars ) ? s : | |
| 91 | s.replaceAll( "\\\\([" + escapeChars | |
| 92 | .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" ); | |
| 93 | } | |
| 94 | ||
| 95 | private static boolean isEmpty( final String s ) { | |
| 96 | return s == null || s.isEmpty(); | |
| 97 | } | |
| 98 | } | |
| 99 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | import com.scrivenvar.AbstractFileFactory; | |
| 31 | import com.scrivenvar.FileType; | |
| 32 | import com.scrivenvar.definition.yaml.YamlDefinitionSource; | |
| 33 | ||
| 34 | import java.nio.file.Path; | |
| 35 | ||
| 36 | import static com.scrivenvar.Constants.GLOB_PREFIX_DEFINITION; | |
| 37 | import static com.scrivenvar.FileType.YAML; | |
| 38 | import static com.scrivenvar.util.ProtocolResolver.getProtocol; | |
| 39 | ||
| 40 | /** | |
| 41 | * Responsible for creating objects that can read and write definition data | |
| 42 | * sources. The data source could be YAML, TOML, JSON, flat files, or from a | |
| 43 | * database. | |
| 44 | */ | |
| 45 | public class DefinitionFactory extends AbstractFileFactory { | |
| 46 | ||
| 47 | /** | |
| 48 | * Default (empty) constructor. | |
| 49 | */ | |
| 50 | public DefinitionFactory() { | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * Creates a definition source capable of reading definitions from the given | |
| 55 | * path. | |
| 56 | * | |
| 57 | * @param path Path to a resource containing definitions. | |
| 58 | * @return The definition source appropriate for the given path. | |
| 59 | */ | |
| 60 | public DefinitionSource createDefinitionSource( final Path path ) { | |
| 61 | assert path != null; | |
| 62 | ||
| 63 | final var protocol = getProtocol( path.toString() ); | |
| 64 | DefinitionSource result = null; | |
| 65 | ||
| 66 | if( protocol.isFile() ) { | |
| 67 | final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION ); | |
| 68 | result = createFileDefinitionSource( filetype, path ); | |
| 69 | } | |
| 70 | else { | |
| 71 | unknownFileType( protocol, path.toString() ); | |
| 72 | } | |
| 73 | ||
| 74 | return result; | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Creates a definition source based on the file type. | |
| 79 | * | |
| 80 | * @param filetype Property key name suffix from settings.properties file. | |
| 81 | * @param path Path to the file that corresponds to the extension. | |
| 82 | * @return A DefinitionSource capable of parsing the data stored at the path. | |
| 83 | */ | |
| 84 | private DefinitionSource createFileDefinitionSource( | |
| 85 | final FileType filetype, final Path path ) { | |
| 86 | assert filetype != null; | |
| 87 | assert path != null; | |
| 88 | ||
| 89 | if( filetype == YAML ) { | |
| 90 | return new YamlDefinitionSource( path ); | |
| 91 | } | |
| 92 | ||
| 93 | throw new IllegalArgumentException( filetype.toString() ); | |
| 94 | } | |
| 95 | } | |
| 96 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; | |
| 31 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 32 | import javafx.beans.property.SimpleStringProperty; | |
| 33 | import javafx.beans.property.StringProperty; | |
| 34 | import javafx.collections.ObservableList; | |
| 35 | import javafx.event.ActionEvent; | |
| 36 | import javafx.event.Event; | |
| 37 | import javafx.event.EventHandler; | |
| 38 | import javafx.geometry.Insets; | |
| 39 | import javafx.geometry.Pos; | |
| 40 | import javafx.scene.Node; | |
| 41 | import javafx.scene.control.*; | |
| 42 | import javafx.scene.input.KeyEvent; | |
| 43 | import javafx.scene.layout.BorderPane; | |
| 44 | import javafx.scene.layout.HBox; | |
| 45 | import javafx.util.StringConverter; | |
| 46 | ||
| 47 | import java.util.*; | |
| 48 | ||
| 49 | import static com.scrivenvar.Messages.get; | |
| 50 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 51 | import static javafx.geometry.Pos.CENTER; | |
| 52 | import static javafx.scene.input.KeyEvent.KEY_PRESSED; | |
| 53 | ||
| 54 | /** | |
| 55 | * Provides the user interface that holds a {@link TreeView}, which | |
| 56 | * allows users to interact with key/value pairs loaded from the | |
| 57 | * {@link DocumentParser} and adapted using a {@link TreeAdapter}. | |
| 58 | */ | |
| 59 | public final class DefinitionPane extends BorderPane { | |
| 60 | ||
| 61 | /** | |
| 62 | * Contains a view of the definitions. | |
| 63 | */ | |
| 64 | private final TreeView<String> mTreeView = new TreeView<>(); | |
| 65 | ||
| 66 | /** | |
| 67 | * Handlers for key press events. | |
| 68 | */ | |
| 69 | private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers | |
| 70 | = new HashSet<>(); | |
| 71 | ||
| 72 | /** | |
| 73 | * Definition file name shown in the title of the pane. | |
| 74 | */ | |
| 75 | private final StringProperty mFilename = new SimpleStringProperty(); | |
| 76 | ||
| 77 | private final TitledPane mTitledPane = new TitledPane(); | |
| 78 | ||
| 79 | /** | |
| 80 | * Constructs a definition pane with a given tree view root. | |
| 81 | */ | |
| 82 | public DefinitionPane() { | |
| 83 | final var treeView = getTreeView(); | |
| 84 | treeView.setEditable( true ); | |
| 85 | treeView.setCellFactory( cell -> createTreeCell() ); | |
| 86 | treeView.setContextMenu( createContextMenu() ); | |
| 87 | treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter ); | |
| 88 | treeView.setShowRoot( false ); | |
| 89 | getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE ); | |
| 90 | ||
| 91 | final var bCreate = createButton( | |
| 92 | "create", TREE, e -> addItem() ); | |
| 93 | final var bRename = createButton( | |
| 94 | "rename", EDIT, e -> editSelectedItem() ); | |
| 95 | final var bDelete = createButton( | |
| 96 | "delete", TRASH, e -> deleteSelectedItems() ); | |
| 97 | ||
| 98 | final var buttonBar = new HBox(); | |
| 99 | buttonBar.getChildren().addAll( bCreate, bRename, bDelete ); | |
| 100 | buttonBar.setAlignment( CENTER ); | |
| 101 | buttonBar.setSpacing( 10 ); | |
| 102 | ||
| 103 | final var titledPane = getTitledPane(); | |
| 104 | titledPane.textProperty().bind( mFilename ); | |
| 105 | titledPane.setContent( treeView ); | |
| 106 | titledPane.setCollapsible( false ); | |
| 107 | titledPane.setPadding( new Insets( 0, 0, 0, 0 ) ); | |
| 108 | ||
| 109 | setTop( buttonBar ); | |
| 110 | setCenter( titledPane ); | |
| 111 | setAlignment( buttonBar, Pos.TOP_CENTER ); | |
| 112 | setAlignment( titledPane, Pos.TOP_CENTER ); | |
| 113 | ||
| 114 | titledPane.prefHeightProperty().bind( this.heightProperty() ); | |
| 115 | } | |
| 116 | ||
| 117 | public void setTooltip( final Tooltip tooltip ) { | |
| 118 | getTitledPane().setTooltip( tooltip ); | |
| 119 | } | |
| 120 | ||
| 121 | private TitledPane getTitledPane() { | |
| 122 | return mTitledPane; | |
| 123 | } | |
| 124 | ||
| 125 | private Button createButton( | |
| 126 | final String msgKey, | |
| 127 | final FontAwesomeIcon icon, | |
| 128 | final EventHandler<ActionEvent> eventHandler ) { | |
| 129 | final var keyPrefix = "Pane.definition.button." + msgKey; | |
| 130 | final var button = new Button( get( keyPrefix + ".label" ) ); | |
| 131 | button.setOnAction( eventHandler ); | |
| 132 | ||
| 133 | button.setGraphic( | |
| 134 | FontAwesomeIconFactory.get().createIcon( icon ) | |
| 135 | ); | |
| 136 | button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) ); | |
| 137 | ||
| 138 | return button; | |
| 139 | } | |
| 140 | ||
| 141 | /** | |
| 142 | * Changes the root of the {@link TreeView} to the root of the | |
| 143 | * {@link TreeView} from the {@link DefinitionSource}. | |
| 144 | * | |
| 145 | * @param definitionSource Container for the hierarchy of key/value pairs | |
| 146 | * to replace the existing hierarchy. | |
| 147 | */ | |
| 148 | public void update( final DefinitionSource definitionSource ) { | |
| 149 | assert definitionSource != null; | |
| 150 | ||
| 151 | final TreeAdapter treeAdapter = definitionSource.getTreeAdapter(); | |
| 152 | final TreeItem<String> root = treeAdapter.adapt( | |
| 153 | get( "Pane.definition.node.root.title" ) | |
| 154 | ); | |
| 155 | ||
| 156 | getTreeView().setRoot( root ); | |
| 157 | } | |
| 158 | ||
| 159 | public Map<String, String> toMap() { | |
| 160 | return TreeItemAdapter.toMap( getTreeView().getRoot() ); | |
| 161 | } | |
| 162 | ||
| 163 | /** | |
| 164 | * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView} | |
| 165 | * is modified. The modifications include: item value changes, item additions, | |
| 166 | * and item removals. | |
| 167 | * <p> | |
| 168 | * Safe to call multiple times; if a handler is already registered, the | |
| 169 | * old handler is used. | |
| 170 | * </p> | |
| 171 | * | |
| 172 | * @param handler The handler to call whenever any {@link TreeItem} changes. | |
| 173 | */ | |
| 174 | public void addTreeChangeHandler( | |
| 175 | final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) { | |
| 176 | final TreeItem<String> root = getTreeView().getRoot(); | |
| 177 | root.addEventHandler( TreeItem.valueChangedEvent(), handler ); | |
| 178 | root.addEventHandler( TreeItem.childrenModificationEvent(), handler ); | |
| 179 | } | |
| 180 | ||
| 181 | public void addKeyEventHandler( | |
| 182 | final EventHandler<? super KeyEvent> handler ) { | |
| 183 | getKeyEventHandlers().add( handler ); | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably | |
| 188 | * well-formed for export. A tree is considered well-formed if the following | |
| 189 | * conditions are met: | |
| 190 | * | |
| 191 | * <ul> | |
| 192 | * <li>The root node contains at least one child node having a leaf.</li> | |
| 193 | * <li>There are no leaf nodes with sibling leaf nodes.</li> | |
| 194 | * </ul> | |
| 195 | * | |
| 196 | * @return {@code null} if the document is well-formed, otherwise the | |
| 197 | * problematic child {@link TreeItem}. | |
| 198 | */ | |
| 199 | public TreeItem<String> isTreeWellFormed() { | |
| 200 | final var root = getTreeView().getRoot(); | |
| 201 | ||
| 202 | for( final var child : root.getChildren() ) { | |
| 203 | final var problemChild = isWellFormed( child ); | |
| 204 | ||
| 205 | if( child.isLeaf() || problemChild != null ) { | |
| 206 | return problemChild; | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | return null; | |
| 211 | } | |
| 212 | ||
| 213 | /** | |
| 214 | * Determines whether the document is well-formed by ensuring that | |
| 215 | * child branches do not contain multiple leaves. | |
| 216 | * | |
| 217 | * @param item The sub-tree to check for well-formedness. | |
| 218 | * @return {@code null} when the tree is well-formed, otherwise the | |
| 219 | * problematic {@link TreeItem}. | |
| 220 | */ | |
| 221 | private TreeItem<String> isWellFormed( final TreeItem<String> item ) { | |
| 222 | int childLeafs = 0; | |
| 223 | int childBranches = 0; | |
| 224 | ||
| 225 | for( final TreeItem<String> child : item.getChildren() ) { | |
| 226 | if( child.isLeaf() ) { | |
| 227 | childLeafs++; | |
| 228 | } | |
| 229 | else { | |
| 230 | childBranches++; | |
| 231 | } | |
| 232 | ||
| 233 | final var problemChild = isWellFormed( child ); | |
| 234 | ||
| 235 | if( problemChild != null ) { | |
| 236 | return problemChild; | |
| 237 | } | |
| 238 | } | |
| 239 | ||
| 240 | return ((childBranches > 0 && childLeafs == 0) || | |
| 241 | (childBranches == 0 && childLeafs <= 1)) ? null : item; | |
| 242 | } | |
| 243 | ||
| 244 | /** | |
| 245 | * Delegates to {@link DefinitionTreeItem#findLeafExact(String)}. | |
| 246 | * | |
| 247 | * @param text The value to find, never {@code null}. | |
| 248 | * @return The leaf that contains the given value, or {@code null} if | |
| 249 | * not found. | |
| 250 | */ | |
| 251 | public DefinitionTreeItem<String> findLeafExact( final String text ) { | |
| 252 | return getTreeRoot().findLeafExact( text ); | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}. | |
| 257 | * | |
| 258 | * @param text The value to find, never {@code null}. | |
| 259 | * @return The leaf that contains the given value, or {@code null} if | |
| 260 | * not found. | |
| 261 | */ | |
| 262 | public DefinitionTreeItem<String> findLeafContains( final String text ) { | |
| 263 | return getTreeRoot().findLeafContains( text ); | |
| 264 | } | |
| 265 | ||
| 266 | /** | |
| 267 | * Delegates to {@link DefinitionTreeItem#findLeafContains(String)}. | |
| 268 | * | |
| 269 | * @param text The value to find, never {@code null}. | |
| 270 | * @return The leaf that contains the given value, or {@code null} if | |
| 271 | * not found. | |
| 272 | */ | |
| 273 | public DefinitionTreeItem<String> findLeafContainsNoCase( | |
| 274 | final String text ) { | |
| 275 | return getTreeRoot().findLeafContainsNoCase( text ); | |
| 276 | } | |
| 277 | ||
| 278 | /** | |
| 279 | * Delegates to {@link DefinitionTreeItem#findLeafStartsWith(String)}. | |
| 280 | * | |
| 281 | * @param text The value to find, never {@code null}. | |
| 282 | * @return The leaf that contains the given value, or {@code null} if | |
| 283 | * not found. | |
| 284 | */ | |
| 285 | public DefinitionTreeItem<String> findLeafStartsWith( final String text ) { | |
| 286 | return getTreeRoot().findLeafStartsWith( text ); | |
| 287 | } | |
| 288 | ||
| 289 | /** | |
| 290 | * Expands the node to the root, recursively. | |
| 291 | * | |
| 292 | * @param <T> The type of tree item to expand (usually String). | |
| 293 | * @param node The node to expand. | |
| 294 | */ | |
| 295 | public <T> void expand( final TreeItem<T> node ) { | |
| 296 | if( node != null ) { | |
| 297 | expand( node.getParent() ); | |
| 298 | ||
| 299 | if( !node.isLeaf() ) { | |
| 300 | node.setExpanded( true ); | |
| 301 | } | |
| 302 | } | |
| 303 | } | |
| 304 | ||
| 305 | public void select( final TreeItem<String> item ) { | |
| 306 | getSelectionModel().clearSelection(); | |
| 307 | getSelectionModel().select( getTreeView().getRow( item ) ); | |
| 308 | } | |
| 309 | ||
| 310 | /** | |
| 311 | * Collapses the tree, recursively. | |
| 312 | */ | |
| 313 | public void collapse() { | |
| 314 | collapse( getTreeRoot().getChildren() ); | |
| 315 | } | |
| 316 | ||
| 317 | /** | |
| 318 | * Collapses the tree, recursively. | |
| 319 | * | |
| 320 | * @param <T> The type of tree item to expand (usually String). | |
| 321 | * @param nodes The nodes to collapse. | |
| 322 | */ | |
| 323 | private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) { | |
| 324 | for( final var node : nodes ) { | |
| 325 | node.setExpanded( false ); | |
| 326 | collapse( node.getChildren() ); | |
| 327 | } | |
| 328 | } | |
| 329 | ||
| 330 | /** | |
| 331 | * @return {@code true} when the user is editing a {@link TreeItem}. | |
| 332 | */ | |
| 333 | private boolean isEditingTreeItem() { | |
| 334 | return getTreeView().editingItemProperty().getValue() != null; | |
| 335 | } | |
| 336 | ||
| 337 | /** | |
| 338 | * Changes to edit mode for the selected item. | |
| 339 | */ | |
| 340 | private void editSelectedItem() { | |
| 341 | getTreeView().edit( getSelectedItem() ); | |
| 342 | } | |
| 343 | ||
| 344 | /** | |
| 345 | * Removes all selected items from the {@link TreeView}. | |
| 346 | */ | |
| 347 | private void deleteSelectedItems() { | |
| 348 | for( final var item : getSelectedItems() ) { | |
| 349 | final var parent = item.getParent(); | |
| 350 | ||
| 351 | if( parent != null ) { | |
| 352 | parent.getChildren().remove( item ); | |
| 353 | } | |
| 354 | } | |
| 355 | } | |
| 356 | ||
| 357 | /** | |
| 358 | * Deletes the selected item. | |
| 359 | */ | |
| 360 | private void deleteSelectedItem() { | |
| 361 | final var c = getSelectedItem(); | |
| 362 | getSiblings( c ).remove( c ); | |
| 363 | } | |
| 364 | ||
| 365 | /** | |
| 366 | * Adds a new item under the selected item (or root if nothing is selected). | |
| 367 | * There are a few conditions to consider: when adding to the root, | |
| 368 | * when adding to a leaf, and when adding to a non-leaf. Items added to the | |
| 369 | * root must contain two items: a key and a value. | |
| 370 | */ | |
| 371 | public void addItem() { | |
| 372 | final var value = createTreeItem(); | |
| 373 | getSelectedItem().getChildren().add( value ); | |
| 374 | expand( value ); | |
| 375 | select( value ); | |
| 376 | } | |
| 377 | ||
| 378 | private ContextMenu createContextMenu() { | |
| 379 | final ContextMenu menu = new ContextMenu(); | |
| 380 | final ObservableList<MenuItem> items = menu.getItems(); | |
| 381 | ||
| 382 | addMenuItem( items, "Definition.menu.create" ) | |
| 383 | .setOnAction( e -> addItem() ); | |
| 384 | ||
| 385 | addMenuItem( items, "Definition.menu.rename" ) | |
| 386 | .setOnAction( e -> editSelectedItem() ); | |
| 387 | ||
| 388 | addMenuItem( items, "Definition.menu.remove" ) | |
| 389 | .setOnAction( e -> deleteSelectedItem() ); | |
| 390 | ||
| 391 | return menu; | |
| 392 | } | |
| 393 | ||
| 394 | /** | |
| 395 | * Executes hot-keys for edits to the definition tree. | |
| 396 | * | |
| 397 | * @param event Contains the key code of the key that was pressed. | |
| 398 | */ | |
| 399 | private void keyEventFilter( final KeyEvent event ) { | |
| 400 | if( !isEditingTreeItem() ) { | |
| 401 | switch( event.getCode() ) { | |
| 402 | case ENTER: | |
| 403 | expand( getSelectedItem() ); | |
| 404 | event.consume(); | |
| 405 | break; | |
| 406 | ||
| 407 | case DELETE: | |
| 408 | deleteSelectedItems(); | |
| 409 | break; | |
| 410 | ||
| 411 | case INSERT: | |
| 412 | addItem(); | |
| 413 | break; | |
| 414 | ||
| 415 | case R: | |
| 416 | if( event.isControlDown() ) { | |
| 417 | editSelectedItem(); | |
| 418 | } | |
| 419 | ||
| 420 | break; | |
| 421 | } | |
| 422 | ||
| 423 | for( final var handler : getKeyEventHandlers() ) { | |
| 424 | handler.handle( event ); | |
| 425 | } | |
| 426 | } | |
| 427 | } | |
| 428 | ||
| 429 | /** | |
| 430 | * Adds a menu item to a list of menu items. | |
| 431 | * | |
| 432 | * @param items The list of menu items to append to. | |
| 433 | * @param labelKey The resource bundle key name for the menu item's label. | |
| 434 | * @return The menu item added to the list of menu items. | |
| 435 | */ | |
| 436 | private MenuItem addMenuItem( | |
| 437 | final List<MenuItem> items, final String labelKey ) { | |
| 438 | final MenuItem menuItem = createMenuItem( labelKey ); | |
| 439 | items.add( menuItem ); | |
| 440 | return menuItem; | |
| 441 | } | |
| 442 | ||
| 443 | private MenuItem createMenuItem( final String labelKey ) { | |
| 444 | return new MenuItem( get( labelKey ) ); | |
| 445 | } | |
| 446 | ||
| 447 | private DefinitionTreeItem<String> createTreeItem() { | |
| 448 | return new DefinitionTreeItem<>( get( "Definition.menu.add.default" ) ); | |
| 449 | } | |
| 450 | ||
| 451 | private TreeCell<String> createTreeCell() { | |
| 452 | return new FocusAwareTextFieldTreeCell( createStringConverter() ) { | |
| 453 | @Override | |
| 454 | public void commitEdit( final String newValue ) { | |
| 455 | super.commitEdit( newValue ); | |
| 456 | select( getTreeItem() ); | |
| 457 | requestFocus(); | |
| 458 | } | |
| 459 | }; | |
| 460 | } | |
| 461 | ||
| 462 | @Override | |
| 463 | public void requestFocus() { | |
| 464 | super.requestFocus(); | |
| 465 | getTreeView().requestFocus(); | |
| 466 | } | |
| 467 | ||
| 468 | private StringConverter<String> createStringConverter() { | |
| 469 | return new StringConverter<>() { | |
| 470 | @Override | |
| 471 | public String toString( final String object ) { | |
| 472 | return object == null ? "" : object; | |
| 473 | } | |
| 474 | ||
| 475 | @Override | |
| 476 | public String fromString( final String string ) { | |
| 477 | return string == null ? "" : string; | |
| 478 | } | |
| 479 | }; | |
| 480 | } | |
| 481 | ||
| 482 | /** | |
| 483 | * Returns the tree view that contains the definition hierarchy. | |
| 484 | * | |
| 485 | * @return A non-null instance. | |
| 486 | */ | |
| 487 | public TreeView<String> getTreeView() { | |
| 488 | return mTreeView; | |
| 489 | } | |
| 490 | ||
| 491 | /** | |
| 492 | * Returns this pane. | |
| 493 | * | |
| 494 | * @return this | |
| 495 | */ | |
| 496 | public Node getNode() { | |
| 497 | return this; | |
| 498 | } | |
| 499 | ||
| 500 | /** | |
| 501 | * Returns the property used to set the title of the pane: the file name. | |
| 502 | * | |
| 503 | * @return A non-null property used for showing the definition file name. | |
| 504 | */ | |
| 505 | public StringProperty filenameProperty() { | |
| 506 | return mFilename; | |
| 507 | } | |
| 508 | ||
| 509 | /** | |
| 510 | * Returns the root of the tree. | |
| 511 | * | |
| 512 | * @return The first node added to the definition tree. | |
| 513 | */ | |
| 514 | private DefinitionTreeItem<String> getTreeRoot() { | |
| 515 | final var root = getTreeView().getRoot(); | |
| 516 | ||
| 517 | return root instanceof DefinitionTreeItem | |
| 518 | ? (DefinitionTreeItem<String>) root | |
| 519 | : new DefinitionTreeItem<>( "root" ); | |
| 520 | } | |
| 521 | ||
| 522 | private ObservableList<TreeItem<String>> getSiblings( | |
| 523 | final TreeItem<String> item ) { | |
| 524 | final var root = getTreeView().getRoot(); | |
| 525 | final var parent = (item == null || item == root) ? root : item.getParent(); | |
| 526 | ||
| 527 | return parent.getChildren(); | |
| 528 | } | |
| 529 | ||
| 530 | private MultipleSelectionModel<TreeItem<String>> getSelectionModel() { | |
| 531 | return getTreeView().getSelectionModel(); | |
| 532 | } | |
| 533 | ||
| 534 | /** | |
| 535 | * Returns a copy of all the selected items. | |
| 536 | * | |
| 537 | * @return A list, possibly empty, containing all selected items in the | |
| 538 | * {@link TreeView}. | |
| 539 | */ | |
| 540 | private List<TreeItem<String>> getSelectedItems() { | |
| 541 | return new ArrayList<>( getSelectionModel().getSelectedItems() ); | |
| 542 | } | |
| 543 | ||
| 544 | public TreeItem<String> getSelectedItem() { | |
| 545 | final var item = getSelectionModel().getSelectedItem(); | |
| 546 | return item == null ? getTreeView().getRoot() : item; | |
| 547 | } | |
| 548 | ||
| 549 | private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() { | |
| 550 | return mKeyEventHandlers; | |
| 551 | } | |
| 552 | ||
| 553 | /** | |
| 554 | * Answers whether there are any definitions in the tree. | |
| 555 | * | |
| 556 | * @return {@code true} when there are no definitions; {@code false} when | |
| 557 | * there's at least one definition. | |
| 558 | */ | |
| 559 | public boolean isEmpty() { | |
| 560 | return getTreeRoot().isEmpty(); | |
| 561 | } | |
| 562 | } | |
| 563 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents behaviours for reading and writing string definitions. This | |
| 32 | * class cannot have any direct hooks into the user interface, as it defines | |
| 33 | * entry points into the definition data model loaded into an object | |
| 34 | * hierarchy. That hierarchy is converted to a UI model using an adapter | |
| 35 | * pattern. | |
| 36 | */ | |
| 37 | public interface DefinitionSource { | |
| 38 | ||
| 39 | /** | |
| 40 | * Creates an object capable of producing view-based objects from this | |
| 41 | * definition source. | |
| 42 | * | |
| 43 | * @return A hierarchical tree suitable for displaying in the definition pane. | |
| 44 | */ | |
| 45 | TreeAdapter getTreeAdapter(); | |
| 46 | } | |
| 47 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | ||
| 32 | import java.util.Stack; | |
| 33 | import java.util.function.BiFunction; | |
| 34 | ||
| 35 | import static java.text.Normalizer.Form.NFD; | |
| 36 | import static java.text.Normalizer.normalize; | |
| 37 | ||
| 38 | /** | |
| 39 | * Provides behaviour afforded to definition keys and corresponding value. | |
| 40 | * | |
| 41 | * @param <T> The type of {@link TreeItem} (usually string). | |
| 42 | */ | |
| 43 | public class DefinitionTreeItem<T> extends TreeItem<T> { | |
| 44 | ||
| 45 | /** | |
| 46 | * Constructs a new item with a default value. | |
| 47 | * | |
| 48 | * @param value Passed up to superclass. | |
| 49 | */ | |
| 50 | public DefinitionTreeItem( final T value ) { | |
| 51 | super( value ); | |
| 52 | } | |
| 53 | ||
| 54 | /** | |
| 55 | * Finds a leaf starting at the current node with text that matches the given | |
| 56 | * value. Search is performed case-sensitively. | |
| 57 | * | |
| 58 | * @param text The text to match against each leaf in the tree. | |
| 59 | * @return The leaf that has a value exactly matching the given text. | |
| 60 | */ | |
| 61 | public DefinitionTreeItem<T> findLeafExact( final String text ) { | |
| 62 | return findLeaf( text, DefinitionTreeItem::valueEquals ); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Finds a leaf starting at the current node with text that matches the given | |
| 67 | * value. Search is performed case-sensitively. | |
| 68 | * | |
| 69 | * @param text The text to match against each leaf in the tree. | |
| 70 | * @return The leaf that has a value that contains the given text. | |
| 71 | */ | |
| 72 | public DefinitionTreeItem<T> findLeafContains( final String text ) { | |
| 73 | return findLeaf( text, DefinitionTreeItem::valueContains ); | |
| 74 | } | |
| 75 | ||
| 76 | /** | |
| 77 | * Finds a leaf starting at the current node with text that matches the given | |
| 78 | * value. Search is performed case-insensitively. | |
| 79 | * | |
| 80 | * @param text The text to match against each leaf in the tree. | |
| 81 | * @return The leaf that has a value that contains the given text. | |
| 82 | */ | |
| 83 | public DefinitionTreeItem<T> findLeafContainsNoCase( final String text ) { | |
| 84 | return findLeaf( text, DefinitionTreeItem::valueContainsNoCase ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Finds a leaf starting at the current node with text that matches the given | |
| 89 | * value. Search is performed case-sensitively. | |
| 90 | * | |
| 91 | * @param text The text to match against each leaf in the tree. | |
| 92 | * @return The leaf that has a value that starts with the given text. | |
| 93 | */ | |
| 94 | public DefinitionTreeItem<T> findLeafStartsWith( final String text ) { | |
| 95 | return findLeaf( text, DefinitionTreeItem::valueStartsWith ); | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Finds a leaf starting at the current node with text that matches the given | |
| 100 | * value. | |
| 101 | * | |
| 102 | * @param text The text to match against each leaf in the tree. | |
| 103 | * @param findMode What algorithm is used to match the given text. | |
| 104 | * @return The leaf that has a value starting with the given text, or {@code | |
| 105 | * null} if there was no match found. | |
| 106 | */ | |
| 107 | public DefinitionTreeItem<T> findLeaf( | |
| 108 | final String text, | |
| 109 | final BiFunction<DefinitionTreeItem<T>, String, Boolean> findMode ) { | |
| 110 | final var stack = new Stack<DefinitionTreeItem<T>>(); | |
| 111 | stack.push( this ); | |
| 112 | ||
| 113 | // Don't hunt for blank (empty) keys. | |
| 114 | boolean found = text.isBlank(); | |
| 115 | ||
| 116 | while( !found && !stack.isEmpty() ) { | |
| 117 | final var node = stack.pop(); | |
| 118 | ||
| 119 | for( final var child : node.getChildren() ) { | |
| 120 | final var result = (DefinitionTreeItem<T>) child; | |
| 121 | ||
| 122 | if( result.isLeaf() ) { | |
| 123 | if( found = findMode.apply( result, text ) ) { | |
| 124 | return result; | |
| 125 | } | |
| 126 | } | |
| 127 | else { | |
| 128 | stack.push( result ); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | ||
| 133 | return null; | |
| 134 | } | |
| 135 | ||
| 136 | /** | |
| 137 | * Returns the value of the string without diacritic marks. | |
| 138 | * | |
| 139 | * @return A non-null, possibly empty string. | |
| 140 | */ | |
| 141 | private String getDiacriticlessValue() { | |
| 142 | return normalize( getValue().toString(), NFD ) | |
| 143 | .replaceAll( "\\p{M}", "" ); | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Returns true if this node is a leaf and its value equals the given text. | |
| 148 | * | |
| 149 | * @param s The text to compare against the node value. | |
| 150 | * @return true Node is a leaf and its value equals the given value. | |
| 151 | */ | |
| 152 | private boolean valueEquals( final String s ) { | |
| 153 | return isLeaf() && getValue().equals( s ); | |
| 154 | } | |
| 155 | ||
| 156 | /** | |
| 157 | * Returns true if this node is a leaf and its value contains the given text. | |
| 158 | * | |
| 159 | * @param s The text to compare against the node value. | |
| 160 | * @return true Node is a leaf and its value contains the given value. | |
| 161 | */ | |
| 162 | private boolean valueContains( final String s ) { | |
| 163 | return isLeaf() && getDiacriticlessValue().contains( s ); | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Returns true if this node is a leaf and its value contains the given text. | |
| 168 | * | |
| 169 | * @param s The text to compare against the node value. | |
| 170 | * @return true Node is a leaf and its value contains the given value. | |
| 171 | */ | |
| 172 | private boolean valueContainsNoCase( final String s ) { | |
| 173 | return isLeaf() && getDiacriticlessValue() | |
| 174 | .toLowerCase().contains( s.toLowerCase() ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Returns true if this node is a leaf and its value starts with the given | |
| 179 | * text. | |
| 180 | * | |
| 181 | * @param s The text to compare against the node value. | |
| 182 | * @return true Node is a leaf and its value starts with the given value. | |
| 183 | */ | |
| 184 | private boolean valueStartsWith( final String s ) { | |
| 185 | return isLeaf() && getDiacriticlessValue().startsWith( s ); | |
| 186 | } | |
| 187 | ||
| 188 | /** | |
| 189 | * Returns the path for this node, with nodes made distinct using the | |
| 190 | * separator character. This uses two loops: one for pushing nodes onto a | |
| 191 | * stack and one for popping them off to create the path in desired order. | |
| 192 | * | |
| 193 | * @return A non-null string, possibly empty. | |
| 194 | */ | |
| 195 | public String toPath() { | |
| 196 | return TreeItemAdapter.toPath( getParent() ); | |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * Answers whether there are any definitions in this tree. | |
| 201 | * | |
| 202 | * @return {@code true} when there are no definitions in the tree; {@code | |
| 203 | * false} when there is at least one definition present. | |
| 204 | */ | |
| 205 | public boolean isEmpty() { | |
| 206 | return getChildren().isEmpty(); | |
| 207 | } | |
| 208 | } | |
| 209 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | /** | |
| 31 | * Responsible for parsing structured document formats. | |
| 32 | * | |
| 33 | * @param <T> The type of "node" for the document's object model. | |
| 34 | */ | |
| 35 | public interface DocumentParser<T> { | |
| 36 | ||
| 37 | /** | |
| 38 | * Parses a document into a nested object hierarchy. The object returned | |
| 39 | * from this call must be the root node in the document tree. | |
| 40 | * | |
| 41 | * @return The document's root node, which may be empty but never null. | |
| 42 | */ | |
| 43 | T getDocumentRoot(); | |
| 44 | } | |
| 45 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | import javafx.scene.Node; | |
| 31 | import javafx.scene.control.TextField; | |
| 32 | import javafx.scene.control.cell.TextFieldTreeCell; | |
| 33 | import javafx.util.StringConverter; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for fixing a focus lost bug in the JavaFX implementation. | |
| 37 | * See https://bugs.openjdk.java.net/browse/JDK-8089514 for details. | |
| 38 | * This implementation borrows from the official documentation on creating | |
| 39 | * tree views: https://docs.oracle.com/javafx/2/ui_controls/tree-view.htm | |
| 40 | */ | |
| 41 | public class FocusAwareTextFieldTreeCell extends TextFieldTreeCell<String> { | |
| 42 | private TextField mTextField; | |
| 43 | ||
| 44 | public FocusAwareTextFieldTreeCell( | |
| 45 | final StringConverter<String> converter ) { | |
| 46 | super( converter ); | |
| 47 | } | |
| 48 | ||
| 49 | @Override | |
| 50 | public void startEdit() { | |
| 51 | super.startEdit(); | |
| 52 | var textField = mTextField; | |
| 53 | ||
| 54 | if( textField == null ) { | |
| 55 | textField = createTextField(); | |
| 56 | } | |
| 57 | else { | |
| 58 | textField.setText( getItem() ); | |
| 59 | } | |
| 60 | ||
| 61 | setText( null ); | |
| 62 | setGraphic( textField ); | |
| 63 | textField.selectAll(); | |
| 64 | textField.requestFocus(); | |
| 65 | ||
| 66 | // When the focus is lost, commit the edit then close the input field. | |
| 67 | // This fixes the unexpected behaviour when user clicks away. | |
| 68 | textField.focusedProperty().addListener( ( l, o, n ) -> { | |
| 69 | if( !n ) { | |
| 70 | commitEdit( mTextField.getText() ); | |
| 71 | } | |
| 72 | } ); | |
| 73 | ||
| 74 | mTextField = textField; | |
| 75 | } | |
| 76 | ||
| 77 | @Override | |
| 78 | public void cancelEdit() { | |
| 79 | super.cancelEdit(); | |
| 80 | setText( getItem() ); | |
| 81 | setGraphic( getTreeItem().getGraphic() ); | |
| 82 | } | |
| 83 | ||
| 84 | @Override | |
| 85 | public void updateItem( String item, boolean empty ) { | |
| 86 | super.updateItem( item, empty ); | |
| 87 | ||
| 88 | String text = null; | |
| 89 | Node graphic = null; | |
| 90 | ||
| 91 | if( !empty ) { | |
| 92 | if( isEditing() ) { | |
| 93 | final var textField = mTextField; | |
| 94 | ||
| 95 | if( textField != null ) { | |
| 96 | textField.setText( getString() ); | |
| 97 | } | |
| 98 | ||
| 99 | graphic = textField; | |
| 100 | } | |
| 101 | else { | |
| 102 | text = getString(); | |
| 103 | graphic = getTreeItem().getGraphic(); | |
| 104 | } | |
| 105 | } | |
| 106 | ||
| 107 | setText( text ); | |
| 108 | setGraphic( graphic ); | |
| 109 | } | |
| 110 | ||
| 111 | private TextField createTextField() { | |
| 112 | final var textField = new TextField( getString() ); | |
| 113 | ||
| 114 | textField.setOnKeyReleased( t -> { | |
| 115 | switch( t.getCode() ) { | |
| 116 | case ENTER -> commitEdit( textField.getText() ); | |
| 117 | case ESCAPE -> cancelEdit(); | |
| 118 | } | |
| 119 | } ); | |
| 120 | ||
| 121 | return textField; | |
| 122 | } | |
| 123 | ||
| 124 | private String getString() { | |
| 125 | return getConverter().toString( getItem() ); | |
| 126 | } | |
| 127 | } | |
| 128 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | import com.scrivenvar.sigils.YamlSigilOperator; | |
| 31 | ||
| 32 | import java.util.Map; | |
| 33 | import java.util.regex.Matcher; | |
| 34 | ||
| 35 | import static com.scrivenvar.sigils.YamlSigilOperator.REGEX_PATTERN; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for performing string interpolation on key/value pairs stored | |
| 39 | * in a map. The values in the map can use a delimited syntax to refer to | |
| 40 | * keys in the map. | |
| 41 | */ | |
| 42 | public class MapInterpolator { | |
| 43 | private static final int GROUP_DELIMITED = 1; | |
| 44 | ||
| 45 | /** | |
| 46 | * Empty. | |
| 47 | */ | |
| 48 | private MapInterpolator() { | |
| 49 | } | |
| 50 | ||
| 51 | /** | |
| 52 | * Performs string interpolation on the values in the given map. This will | |
| 53 | * change any value in the map that contains a variable that matches | |
| 54 | * {@link YamlSigilOperator#REGEX_PATTERN}. | |
| 55 | * | |
| 56 | * @param map Contains values that represent references to keys. | |
| 57 | */ | |
| 58 | public static void interpolate( final Map<String, String> map ) { | |
| 59 | map.replaceAll( ( k, v ) -> resolve( map, v ) ); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Given a value with zero or more key references, this will resolve all | |
| 64 | * the values, recursively. If a key cannot be dereferenced, the value will | |
| 65 | * contain the key name. | |
| 66 | * | |
| 67 | * @param map Map to search for keys when resolving key references. | |
| 68 | * @param value Value containing zero or more key references | |
| 69 | * @return The given value with all embedded key references interpolated. | |
| 70 | */ | |
| 71 | private static String resolve( | |
| 72 | final Map<String, String> map, String value ) { | |
| 73 | final Matcher matcher = REGEX_PATTERN.matcher( value ); | |
| 74 | ||
| 75 | while( matcher.find() ) { | |
| 76 | final String keyName = matcher.group( GROUP_DELIMITED ); | |
| 77 | final String mapValue = map.get( keyName ); | |
| 78 | final String keyValue = mapValue == null | |
| 79 | ? keyName | |
| 80 | : resolve( map, mapValue ); | |
| 81 | ||
| 82 | value = value.replace( keyName, keyValue ); | |
| 83 | } | |
| 84 | ||
| 85 | return value; | |
| 86 | } | |
| 87 | } | |
| 88 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | import javafx.scene.control.TreeView; | |
| 32 | ||
| 33 | /** | |
| 34 | * Indicates that this is the top-most {@link TreeItem}. This class allows | |
| 35 | * the {@link TreeItemAdapter} to ignore the topmost definition. Such | |
| 36 | * contortions are necessary because {@link TreeView} requires a root item | |
| 37 | * that isn't part of the user's definition file. | |
| 38 | * <p> | |
| 39 | * Another approach would be to associate object pairs per {@link TreeItem}, | |
| 40 | * but that would be a waste of memory since the only "exception" case is | |
| 41 | * the root {@link TreeItem}. | |
| 42 | * </p> | |
| 43 | * | |
| 44 | * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}. | |
| 45 | */ | |
| 46 | public class RootTreeItem<T> extends DefinitionTreeItem<T> { | |
| 47 | /** | |
| 48 | * Default constructor, calls the superclass, no other behaviour. | |
| 49 | * | |
| 50 | * @param value The {@link TreeItem} node name to construct the superclass. | |
| 51 | * @see TreeItemAdapter#toMap(TreeItem) for details on how this | |
| 52 | * class is used. | |
| 53 | */ | |
| 54 | public RootTreeItem( final T value ) { | |
| 55 | super( value ); | |
| 56 | } | |
| 57 | } | |
| 58 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | import javafx.scene.control.TreeItem; | |
| 31 | ||
| 32 | import java.io.IOException; | |
| 33 | import java.nio.file.Path; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for converting an object hierarchy into a {@link TreeItem} | |
| 37 | * hierarchy. | |
| 38 | */ | |
| 39 | public interface TreeAdapter { | |
| 40 | /** | |
| 41 | * Adapts the document produced by the given parser into a {@link TreeItem} | |
| 42 | * object that can be presented to the user within a GUI. | |
| 43 | * | |
| 44 | * @param root The default root node name. | |
| 45 | * @return The parsed document in a {@link TreeItem} that can be displayed | |
| 46 | * in a panel. | |
| 47 | */ | |
| 48 | TreeItem<String> adapt( String root ); | |
| 49 | ||
| 50 | /** | |
| 51 | * Exports the given root node to the given path. | |
| 52 | * | |
| 53 | * @param root The root node to export. | |
| 54 | * @param path Where to persist the data. | |
| 55 | * @throws IOException Could not write the data to the given path. | |
| 56 | */ | |
| 57 | void export( TreeItem<String> root, Path path ) throws IOException; | |
| 58 | } | |
| 59 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.scrivenvar.sigils.YamlSigilOperator; | |
| 32 | import com.scrivenvar.preview.HTMLPreviewPane; | |
| 33 | import javafx.scene.control.TreeItem; | |
| 34 | import javafx.scene.control.TreeView; | |
| 35 | ||
| 36 | import java.util.HashMap; | |
| 37 | import java.util.Iterator; | |
| 38 | import java.util.Map; | |
| 39 | import java.util.Stack; | |
| 40 | ||
| 41 | import static com.scrivenvar.Constants.DEFAULT_MAP_SIZE; | |
| 42 | ||
| 43 | /** | |
| 44 | * Given a {@link TreeItem}, this will generate a flat map with all the | |
| 45 | * values in the tree recursively interpolated. The application integrates | |
| 46 | * definition files as follows: | |
| 47 | * <ol> | |
| 48 | * <li>Load YAML file into {@link JsonNode} hierarchy.</li> | |
| 49 | * <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li> | |
| 50 | * <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li> | |
| 51 | * <li>Substitute flat map variables into document as required.</li> | |
| 52 | * </ol> | |
| 53 | * | |
| 54 | * <p> | |
| 55 | * This class is responsible for producing the interpolated flat map. This | |
| 56 | * allows dynamic edits of the {@link TreeView} to be displayed in the | |
| 57 | * {@link HTMLPreviewPane} without having to reload the definition file. | |
| 58 | * Reloading the definition file would work, but has a number of drawbacks. | |
| 59 | * </p> | |
| 60 | */ | |
| 61 | public class TreeItemAdapter { | |
| 62 | /** | |
| 63 | * Separates YAML definition keys (e.g., the dots in {@code $root.node.var$}). | |
| 64 | */ | |
| 65 | public static final String SEPARATOR = "."; | |
| 66 | ||
| 67 | /** | |
| 68 | * Default buffer length for keys ({@link StringBuilder} has 16 character | |
| 69 | * buffer) that should be large enough for most keys to avoid reallocating | |
| 70 | * memory to increase the {@link StringBuilder}'s buffer. | |
| 71 | */ | |
| 72 | public static final int DEFAULT_KEY_LENGTH = 64; | |
| 73 | ||
| 74 | /** | |
| 75 | * In-order traversal of a {@link TreeItem} hierarchy, exposing each item | |
| 76 | * as a consecutive list. | |
| 77 | */ | |
| 78 | private static final class TreeIterator | |
| 79 | implements Iterator<TreeItem<String>> { | |
| 80 | private final Stack<TreeItem<String>> mStack = new Stack<>(); | |
| 81 | ||
| 82 | public TreeIterator( final TreeItem<String> root ) { | |
| 83 | if( root != null ) { | |
| 84 | mStack.push( root ); | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | @Override | |
| 89 | public boolean hasNext() { | |
| 90 | return !mStack.isEmpty(); | |
| 91 | } | |
| 92 | ||
| 93 | @Override | |
| 94 | public TreeItem<String> next() { | |
| 95 | final TreeItem<String> next = mStack.pop(); | |
| 96 | next.getChildren().forEach( mStack::push ); | |
| 97 | ||
| 98 | return next; | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | private TreeItemAdapter() { | |
| 103 | } | |
| 104 | ||
| 105 | /** | |
| 106 | * Iterate over a given root node (at any level of the tree) and process each | |
| 107 | * leaf node into a flat map. Values must be interpolated separately. | |
| 108 | */ | |
| 109 | public static Map<String, String> toMap( final TreeItem<String> root ) { | |
| 110 | final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE ); | |
| 111 | final TreeIterator iterator = new TreeIterator( root ); | |
| 112 | ||
| 113 | iterator.forEachRemaining( item -> { | |
| 114 | if( item.isLeaf() ) { | |
| 115 | map.put( toPath( item.getParent() ), item.getValue() ); | |
| 116 | } | |
| 117 | } ); | |
| 118 | ||
| 119 | return map; | |
| 120 | } | |
| 121 | ||
| 122 | ||
| 123 | /** | |
| 124 | * For a given node, this will ascend the tree to generate a key name | |
| 125 | * that is associated with the leaf node's value. | |
| 126 | * | |
| 127 | * @param node Ascendants represent the key to this node's value. | |
| 128 | * @param <T> Data type that the {@link TreeItem} contains. | |
| 129 | * @return The string representation of the node's unique key. | |
| 130 | */ | |
| 131 | public static <T> String toPath( TreeItem<T> node ) { | |
| 132 | assert node != null; | |
| 133 | ||
| 134 | final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH ); | |
| 135 | final Stack<TreeItem<T>> stack = new Stack<>(); | |
| 136 | ||
| 137 | while( node != null && !(node instanceof RootTreeItem) ) { | |
| 138 | stack.push( node ); | |
| 139 | node = node.getParent(); | |
| 140 | } | |
| 141 | ||
| 142 | // Gets set at end of first iteration (to avoid an if condition). | |
| 143 | String separator = ""; | |
| 144 | ||
| 145 | while( !stack.empty() ) { | |
| 146 | final T subkey = stack.pop().getValue(); | |
| 147 | key.append( separator ); | |
| 148 | key.append( subkey ); | |
| 149 | separator = SEPARATOR; | |
| 150 | } | |
| 151 | ||
| 152 | return YamlSigilOperator.entoken( key.toString() ); | |
| 153 | } | |
| 154 | } | |
| 155 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition.yaml; | |
| 29 | ||
| 30 | import com.scrivenvar.definition.DefinitionSource; | |
| 31 | import com.scrivenvar.definition.TreeAdapter; | |
| 32 | ||
| 33 | import java.nio.file.Path; | |
| 34 | ||
| 35 | /** | |
| 36 | * Represents a definition data source for YAML files. | |
| 37 | */ | |
| 38 | public class YamlDefinitionSource implements DefinitionSource { | |
| 39 | ||
| 40 | private final YamlTreeAdapter mYamlTreeAdapter; | |
| 41 | ||
| 42 | /** | |
| 43 | * Constructs a new YAML definition source, populated from the given file. | |
| 44 | * | |
| 45 | * @param path Path to the YAML definition file. | |
| 46 | */ | |
| 47 | public YamlDefinitionSource( final Path path ) { | |
| 48 | assert path != null; | |
| 49 | ||
| 50 | mYamlTreeAdapter = new YamlTreeAdapter( path ); | |
| 51 | } | |
| 52 | ||
| 53 | @Override | |
| 54 | public TreeAdapter getTreeAdapter() { | |
| 55 | return mYamlTreeAdapter; | |
| 56 | } | |
| 57 | } | |
| 58 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition.yaml; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 32 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; | |
| 33 | import com.scrivenvar.definition.DocumentParser; | |
| 34 | ||
| 35 | import java.io.InputStream; | |
| 36 | import java.nio.file.Files; | |
| 37 | import java.nio.file.Path; | |
| 38 | ||
| 39 | /** | |
| 40 | * Responsible for reading a YAML document into an object hierarchy. | |
| 41 | */ | |
| 42 | public class YamlParser implements DocumentParser<JsonNode> { | |
| 43 | ||
| 44 | /** | |
| 45 | * Start of the Universe (the YAML document node that contains all others). | |
| 46 | */ | |
| 47 | private final JsonNode mDocumentRoot; | |
| 48 | ||
| 49 | /** | |
| 50 | * Creates a new YamlParser instance that attempts to parse the contents | |
| 51 | * of the YAML document given from a path. In the event that the file either | |
| 52 | * does not exist or is empty, a fake | |
| 53 | * | |
| 54 | * @param path Path to a file containing YAML data to parse. | |
| 55 | */ | |
| 56 | public YamlParser( final Path path ) { | |
| 57 | assert path != null; | |
| 58 | mDocumentRoot = parse( path ); | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Returns the parent node for the entire YAML document tree. | |
| 63 | * | |
| 64 | * @return The document root, never {@code null}. | |
| 65 | */ | |
| 66 | @Override | |
| 67 | public JsonNode getDocumentRoot() { | |
| 68 | return mDocumentRoot; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Parses the given path containing YAML data into an object hierarchy. | |
| 73 | * | |
| 74 | * @param path {@link Path} to the YAML resource to parse. | |
| 75 | * @return The parsed contents, or an empty object hierarchy. | |
| 76 | */ | |
| 77 | private JsonNode parse( final Path path ) { | |
| 78 | try( final InputStream in = Files.newInputStream( path ) ) { | |
| 79 | return new ObjectMapper( new YAMLFactory() ).readTree( in ); | |
| 80 | } catch( final Exception e ) { | |
| 81 | // Ensure that a document root node exists by relying on the | |
| 82 | // default failure condition when processing. This is required | |
| 83 | // because the input stream could not be read. | |
| 84 | return new ObjectMapper().createObjectNode(); | |
| 85 | } | |
| 86 | } | |
| 87 | } | |
| 88 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.definition.yaml; | |
| 29 | ||
| 30 | import com.fasterxml.jackson.databind.JsonNode; | |
| 31 | import com.fasterxml.jackson.databind.node.ObjectNode; | |
| 32 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; | |
| 33 | import com.scrivenvar.definition.RootTreeItem; | |
| 34 | import com.scrivenvar.definition.TreeAdapter; | |
| 35 | import com.scrivenvar.definition.DefinitionTreeItem; | |
| 36 | import javafx.scene.control.TreeItem; | |
| 37 | import javafx.scene.control.TreeView; | |
| 38 | ||
| 39 | import java.io.IOException; | |
| 40 | import java.nio.file.Path; | |
| 41 | import java.util.Map.Entry; | |
| 42 | ||
| 43 | /** | |
| 44 | * Transforms a JsonNode hierarchy into a tree that can be displayed in a user | |
| 45 | * interface and vice-versa. | |
| 46 | */ | |
| 47 | public class YamlTreeAdapter implements TreeAdapter { | |
| 48 | private final YamlParser mParser; | |
| 49 | ||
| 50 | /** | |
| 51 | * Constructs a new instance that will use the given path to read | |
| 52 | * the object hierarchy from a data source. | |
| 53 | * | |
| 54 | * @param path Path to YAML contents to parse. | |
| 55 | */ | |
| 56 | public YamlTreeAdapter( final Path path ) { | |
| 57 | mParser = new YamlParser( path ); | |
| 58 | } | |
| 59 | ||
| 60 | @Override | |
| 61 | public void export( final TreeItem<String> treeItem, final Path path ) | |
| 62 | throws IOException { | |
| 63 | final YAMLMapper mapper = new YAMLMapper(); | |
| 64 | final ObjectNode root = mapper.createObjectNode(); | |
| 65 | ||
| 66 | // Iterate over the root item's children. The root item is used by the | |
| 67 | // application to ensure definitions can always be added to a tree, as | |
| 68 | // such it is not meant to be exported, only its children. | |
| 69 | for( final TreeItem<String> child : treeItem.getChildren() ) { | |
| 70 | export( child, root ); | |
| 71 | } | |
| 72 | ||
| 73 | // Writes as UTF8 by default. | |
| 74 | mapper.writeValue( path.toFile(), root ); | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Recursive method to generate an object hierarchy that represents the | |
| 79 | * given {@link TreeItem} hierarchy. | |
| 80 | * | |
| 81 | * @param item The {@link TreeItem} to reproduce as an object hierarchy. | |
| 82 | * @param node The {@link ObjectNode} to update to reflect the | |
| 83 | * {@link TreeItem} hierarchy. | |
| 84 | */ | |
| 85 | private void export( final TreeItem<String> item, ObjectNode node ) { | |
| 86 | final var children = item.getChildren(); | |
| 87 | ||
| 88 | // If the current item has more than one non-leaf child, it's an | |
| 89 | // object node and must become a new nested object. | |
| 90 | if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) { | |
| 91 | node = node.putObject( item.getValue() ); | |
| 92 | } | |
| 93 | ||
| 94 | for( final TreeItem<String> child : children ) { | |
| 95 | if( child.isLeaf() ) { | |
| 96 | node.put( item.getValue(), child.getValue() ); | |
| 97 | } | |
| 98 | else { | |
| 99 | export( child, node ); | |
| 100 | } | |
| 101 | } | |
| 102 | } | |
| 103 | ||
| 104 | /** | |
| 105 | * Converts a YAML document to a {@link TreeItem} based on the document | |
| 106 | * keys. Only the first document in the stream is adapted. | |
| 107 | * | |
| 108 | * @param root Root {@link TreeItem} node name. | |
| 109 | * @return A {@link TreeItem} populated with all the keys in the YAML | |
| 110 | * document. | |
| 111 | */ | |
| 112 | public TreeItem<String> adapt( final String root ) { | |
| 113 | final JsonNode rootNode = getYamlParser().getDocumentRoot(); | |
| 114 | final TreeItem<String> rootItem = createRootTreeItem( root ); | |
| 115 | ||
| 116 | rootItem.setExpanded( true ); | |
| 117 | adapt( rootNode, rootItem ); | |
| 118 | return rootItem; | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Iterate over a given root node (at any level of the tree) and adapt each | |
| 123 | * leaf node. | |
| 124 | * | |
| 125 | * @param rootNode A JSON node (YAML node) to adapt. | |
| 126 | * @param rootItem The tree item to use as the root when processing the node. | |
| 127 | */ | |
| 128 | private void adapt( | |
| 129 | final JsonNode rootNode, final TreeItem<String> rootItem ) { | |
| 130 | rootNode.fields().forEachRemaining( | |
| 131 | ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem ) | |
| 132 | ); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Recursively adapt each rootNode to a corresponding rootItem. | |
| 137 | * | |
| 138 | * @param rootNode The node to adapt. | |
| 139 | * @param rootItem The item to adapt using the node's key. | |
| 140 | */ | |
| 141 | private void adapt( | |
| 142 | final Entry<String, JsonNode> rootNode, | |
| 143 | final TreeItem<String> rootItem ) { | |
| 144 | final JsonNode leafNode = rootNode.getValue(); | |
| 145 | final String key = rootNode.getKey(); | |
| 146 | final TreeItem<String> leaf = createTreeItem( key ); | |
| 147 | ||
| 148 | if( leafNode.isValueNode() ) { | |
| 149 | leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) ); | |
| 150 | } | |
| 151 | ||
| 152 | rootItem.getChildren().add( leaf ); | |
| 153 | ||
| 154 | if( leafNode.isObject() ) { | |
| 155 | adapt( leafNode, leaf ); | |
| 156 | } | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * Creates a new {@link TreeItem} that can be added to the {@link TreeView}. | |
| 161 | * | |
| 162 | * @param value The node's value. | |
| 163 | * @return A new {@link TreeItem}, never {@code null}. | |
| 164 | */ | |
| 165 | private TreeItem<String> createTreeItem( final String value ) { | |
| 166 | return new DefinitionTreeItem<>( value ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Creates a new {@link TreeItem} that is intended to be the root-level item | |
| 171 | * added to the {@link TreeView}. This allows the root item to be | |
| 172 | * distinguished from the other items so that reference keys do not include | |
| 173 | * "Definition" as part of their name. | |
| 174 | * | |
| 175 | * @param value The node's value. | |
| 176 | * @return A new {@link TreeItem}, never {@code null}. | |
| 177 | */ | |
| 178 | private TreeItem<String> createRootTreeItem( final String value ) { | |
| 179 | return new RootTreeItem<>( value ); | |
| 180 | } | |
| 181 | ||
| 182 | public YamlParser getYamlParser() { | |
| 183 | return mParser; | |
| 184 | } | |
| 185 | } | |
| 186 | 1 |
| 1 | /* | |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.dialogs; | |
| 29 | ||
| 30 | import static com.scrivenvar.Messages.get; | |
| 31 | import com.scrivenvar.service.events.impl.ButtonOrderPane; | |
| 32 | import static javafx.scene.control.ButtonType.CANCEL; | |
| 33 | import static javafx.scene.control.ButtonType.OK; | |
| 34 | import javafx.scene.control.Dialog; | |
| 35 | import javafx.stage.Window; | |
| 36 | ||
| 37 | /** | |
| 38 | * Superclass that abstracts common behaviours for all dialogs. | |
| 39 | * | |
| 40 | * @param <T> The type of dialog to create (usually String). | |
| 41 | */ | |
| 42 | public abstract class AbstractDialog<T> extends Dialog<T> { | |
| 43 | ||
| 44 | /** | |
| 45 | * Ensures that all dialogs can be closed. | |
| 46 | * | |
| 47 | * @param owner The parent window of this dialog. | |
| 48 | * @param title The messages title to display in the title bar. | |
| 49 | */ | |
| 50 | @SuppressWarnings( "OverridableMethodCallInConstructor" ) | |
| 51 | public AbstractDialog( final Window owner, final String title ) { | |
| 52 | setTitle( get( title ) ); | |
| 53 | setResizable( true ); | |
| 54 | ||
| 55 | initOwner( owner ); | |
| 56 | initCloseAction(); | |
| 57 | initDialogPane(); | |
| 58 | initDialogButtons(); | |
| 59 | initComponents(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Initialize the component layout. | |
| 64 | */ | |
| 65 | protected abstract void initComponents(); | |
| 66 | ||
| 67 | /** | |
| 68 | * Set the dialog to use a button order pane with an OK and a CANCEL button. | |
| 69 | */ | |
| 70 | protected void initDialogPane() { | |
| 71 | setDialogPane( new ButtonOrderPane() ); | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Set an OK and CANCEL button on the dialog. | |
| 76 | */ | |
| 77 | protected void initDialogButtons() { | |
| 78 | getDialogPane().getButtonTypes().addAll( OK, CANCEL ); | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Attaches a setOnCloseRequest to the dialog's [X] button so that the user | |
| 83 | * can always close the window, even if there's an error. | |
| 84 | */ | |
| 85 | protected final void initCloseAction() { | |
| 86 | final Window window = getDialogPane().getScene().getWindow(); | |
| 87 | window.setOnCloseRequest( event -> window.hide() ); | |
| 88 | } | |
| 89 | } | |
| 90 | 1 |
| 1 | /* | |
| 2 | * Copyright 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.scrivenvar.dialogs; | |
| 28 | ||
| 29 | import static com.scrivenvar.Messages.get; | |
| 30 | import com.scrivenvar.controls.BrowseFileButton; | |
| 31 | import com.scrivenvar.controls.EscapeTextField; | |
| 32 | import java.nio.file.Path; | |
| 33 | import javafx.application.Platform; | |
| 34 | import javafx.beans.binding.Bindings; | |
| 35 | import javafx.beans.property.SimpleStringProperty; | |
| 36 | import javafx.beans.property.StringProperty; | |
| 37 | import javafx.scene.control.ButtonBar.ButtonData; | |
| 38 | import static javafx.scene.control.ButtonType.OK; | |
| 39 | import javafx.scene.control.DialogPane; | |
| 40 | import javafx.scene.control.Label; | |
| 41 | import javafx.stage.FileChooser.ExtensionFilter; | |
| 42 | import javafx.stage.Window; | |
| 43 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 44 | ||
| 45 | /** | |
| 46 | * Dialog to enter a markdown image. | |
| 47 | */ | |
| 48 | public class ImageDialog extends AbstractDialog<String> { | |
| 49 | ||
| 50 | private final StringProperty image = new SimpleStringProperty(); | |
| 51 | ||
| 52 | public ImageDialog( final Window owner, final Path basePath ) { | |
| 53 | super(owner, "Dialog.image.title" ); | |
| 54 | ||
| 55 | final DialogPane dialogPane = getDialogPane(); | |
| 56 | dialogPane.setContent( pane ); | |
| 57 | ||
| 58 | linkBrowseFileButton.setBasePath( basePath ); | |
| 59 | linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) ); | |
| 60 | linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() ); | |
| 61 | ||
| 62 | dialogPane.lookupButton( OK ).disableProperty().bind( | |
| 63 | urlField.escapedTextProperty().isEmpty() | |
| 64 | .or( textField.escapedTextProperty().isEmpty() ) ); | |
| 65 | ||
| 66 | image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | |
| 67 | .then( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | |
| 68 | .otherwise( Bindings.format( "", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) ); | |
| 69 | previewField.textProperty().bind( image ); | |
| 70 | ||
| 71 | setResultConverter( dialogButton -> { | |
| 72 | ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null; | |
| 73 | return (data == ButtonData.OK_DONE) ? image.get() : null; | |
| 74 | } ); | |
| 75 | ||
| 76 | Platform.runLater( () -> { | |
| 77 | urlField.requestFocus(); | |
| 78 | ||
| 79 | if( urlField.getText().startsWith( "http://" ) ) { | |
| 80 | urlField.selectRange( "http://".length(), urlField.getLength() ); | |
| 81 | } | |
| 82 | } ); | |
| 83 | } | |
| 84 | ||
| 85 | @Override | |
| 86 | protected void initComponents() { | |
| 87 | // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | |
| 88 | pane = new MigPane(); | |
| 89 | Label urlLabel = new Label(); | |
| 90 | urlField = new EscapeTextField(); | |
| 91 | linkBrowseFileButton = new BrowseFileButton(); | |
| 92 | Label textLabel = new Label(); | |
| 93 | textField = new EscapeTextField(); | |
| 94 | Label titleLabel = new Label(); | |
| 95 | titleField = new EscapeTextField(); | |
| 96 | Label previewLabel = new Label(); | |
| 97 | previewField = new Label(); | |
| 98 | ||
| 99 | //======== pane ======== | |
| 100 | { | |
| 101 | pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" ); | |
| 102 | pane.setRows( "[][][][]" ); | |
| 103 | ||
| 104 | //---- urlLabel ---- | |
| 105 | urlLabel.setText( get( "Dialog.image.urlLabel.text" ) ); | |
| 106 | pane.add( urlLabel, "cell 0 0" ); | |
| 107 | ||
| 108 | //---- urlField ---- | |
| 109 | urlField.setEscapeCharacters( "()" ); | |
| 110 | urlField.setText( "http://yourlink.com" ); | |
| 111 | urlField.setPromptText( "http://yourlink.com" ); | |
| 112 | pane.add( urlField, "cell 1 0" ); | |
| 113 | pane.add( linkBrowseFileButton, "cell 2 0" ); | |
| 114 | ||
| 115 | //---- textLabel ---- | |
| 116 | textLabel.setText( get( "Dialog.image.textLabel.text" ) ); | |
| 117 | pane.add( textLabel, "cell 0 1" ); | |
| 118 | ||
| 119 | //---- textField ---- | |
| 120 | textField.setEscapeCharacters( "[]" ); | |
| 121 | pane.add( textField, "cell 1 1 2 1" ); | |
| 122 | ||
| 123 | //---- titleLabel ---- | |
| 124 | titleLabel.setText( get( "Dialog.image.titleLabel.text" ) ); | |
| 125 | pane.add( titleLabel, "cell 0 2" ); | |
| 126 | pane.add( titleField, "cell 1 2 2 1" ); | |
| 127 | ||
| 128 | //---- previewLabel ---- | |
| 129 | previewLabel.setText( get( "Dialog.image.previewLabel.text" ) ); | |
| 130 | pane.add( previewLabel, "cell 0 3" ); | |
| 131 | pane.add( previewField, "cell 1 3 2 1" ); | |
| 132 | } | |
| 133 | // JFormDesigner - End of component initialization //GEN-END:initComponents | |
| 134 | } | |
| 135 | ||
| 136 | // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | |
| 137 | private MigPane pane; | |
| 138 | private EscapeTextField urlField; | |
| 139 | private BrowseFileButton linkBrowseFileButton; | |
| 140 | private EscapeTextField textField; | |
| 141 | private EscapeTextField titleField; | |
| 142 | private Label previewField; | |
| 143 | // JFormDesigner - End of variables declaration //GEN-END:variables | |
| 144 | } | |
| 145 | 1 |
| 1 | JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | |
| 2 | ||
| 3 | new FormModel { | |
| 4 | "i18n.bundlePackage": "com.scrivendor" | |
| 5 | "i18n.bundleName": "messages" | |
| 6 | "i18n.autoExternalize": true | |
| 7 | "i18n.keyPrefix": "ImageDialog" | |
| 8 | contentType: "form/javafx" | |
| 9 | root: new FormRoot { | |
| 10 | add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | |
| 11 | "$layoutConstraints": "" | |
| 12 | "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]" | |
| 13 | "$rowConstraints": "[][][][]" | |
| 14 | } ) { | |
| 15 | name: "pane" | |
| 16 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 17 | name: "urlLabel" | |
| 18 | "text": new FormMessage( null, "ImageDialog.urlLabel.text" ) | |
| 19 | auxiliary() { | |
| 20 | "JavaCodeGenerator.variableLocal": true | |
| 21 | } | |
| 22 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 23 | "value": "cell 0 0" | |
| 24 | } ) | |
| 25 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 26 | name: "urlField" | |
| 27 | "escapeCharacters": "()" | |
| 28 | "text": "http://yourlink.com" | |
| 29 | "promptText": "http://yourlink.com" | |
| 30 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 31 | "value": "cell 1 0" | |
| 32 | } ) | |
| 33 | add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) { | |
| 34 | name: "linkBrowseFileButton" | |
| 35 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 36 | "value": "cell 2 0" | |
| 37 | } ) | |
| 38 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 39 | name: "textLabel" | |
| 40 | "text": new FormMessage( null, "ImageDialog.textLabel.text" ) | |
| 41 | auxiliary() { | |
| 42 | "JavaCodeGenerator.variableLocal": true | |
| 43 | } | |
| 44 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 45 | "value": "cell 0 1" | |
| 46 | } ) | |
| 47 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 48 | name: "textField" | |
| 49 | "escapeCharacters": "[]" | |
| 50 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 51 | "value": "cell 1 1 2 1" | |
| 52 | } ) | |
| 53 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 54 | name: "titleLabel" | |
| 55 | "text": new FormMessage( null, "ImageDialog.titleLabel.text" ) | |
| 56 | auxiliary() { | |
| 57 | "JavaCodeGenerator.variableLocal": true | |
| 58 | } | |
| 59 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 60 | "value": "cell 0 2" | |
| 61 | } ) | |
| 62 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 63 | name: "titleField" | |
| 64 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 65 | "value": "cell 1 2 2 1" | |
| 66 | } ) | |
| 67 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 68 | name: "previewLabel" | |
| 69 | "text": new FormMessage( null, "ImageDialog.previewLabel.text" ) | |
| 70 | auxiliary() { | |
| 71 | "JavaCodeGenerator.variableLocal": true | |
| 72 | } | |
| 73 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 74 | "value": "cell 0 3" | |
| 75 | } ) | |
| 76 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 77 | name: "previewField" | |
| 78 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 79 | "value": "cell 1 3 2 1" | |
| 80 | } ) | |
| 81 | }, new FormLayoutConstraints( null ) { | |
| 82 | "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | |
| 83 | "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | |
| 84 | } ) | |
| 85 | } | |
| 86 | } | |
| 87 | 1 |
| 1 | /* | |
| 2 | * Copyright 2016 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.dialogs; | |
| 29 | ||
| 30 | import com.scrivenvar.controls.EscapeTextField; | |
| 31 | import com.scrivenvar.editors.markdown.HyperlinkModel; | |
| 32 | import javafx.application.Platform; | |
| 33 | import javafx.beans.binding.Bindings; | |
| 34 | import javafx.beans.property.SimpleStringProperty; | |
| 35 | import javafx.beans.property.StringProperty; | |
| 36 | import javafx.scene.control.ButtonBar.ButtonData; | |
| 37 | import javafx.scene.control.DialogPane; | |
| 38 | import javafx.scene.control.Label; | |
| 39 | import javafx.stage.Window; | |
| 40 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 41 | ||
| 42 | import static com.scrivenvar.Messages.get; | |
| 43 | import static javafx.scene.control.ButtonType.OK; | |
| 44 | ||
| 45 | /** | |
| 46 | * Dialog to enter a markdown link. | |
| 47 | */ | |
| 48 | public class LinkDialog extends AbstractDialog<String> { | |
| 49 | ||
| 50 | private final StringProperty link = new SimpleStringProperty(); | |
| 51 | ||
| 52 | public LinkDialog( | |
| 53 | final Window owner, final HyperlinkModel hyperlink ) { | |
| 54 | super( owner, "Dialog.link.title" ); | |
| 55 | ||
| 56 | final DialogPane dialogPane = getDialogPane(); | |
| 57 | dialogPane.setContent( pane ); | |
| 58 | ||
| 59 | dialogPane.lookupButton( OK ).disableProperty().bind( | |
| 60 | urlField.escapedTextProperty().isEmpty() ); | |
| 61 | ||
| 62 | textField.setText( hyperlink.getText() ); | |
| 63 | urlField.setText( hyperlink.getUrl() ); | |
| 64 | titleField.setText( hyperlink.getTitle() ); | |
| 65 | ||
| 66 | link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() ) | |
| 67 | .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) ) | |
| 68 | .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() ) | |
| 69 | .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) | |
| 70 | .otherwise( urlField.escapedTextProperty() ) ) ); | |
| 71 | ||
| 72 | setResultConverter( dialogButton -> { | |
| 73 | ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null; | |
| 74 | return (data == ButtonData.OK_DONE) ? link.get() : null; | |
| 75 | } ); | |
| 76 | ||
| 77 | Platform.runLater( () -> { | |
| 78 | urlField.requestFocus(); | |
| 79 | urlField.selectRange( 0, urlField.getLength() ); | |
| 80 | } ); | |
| 81 | } | |
| 82 | ||
| 83 | @Override | |
| 84 | protected void initComponents() { | |
| 85 | // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents | |
| 86 | pane = new MigPane(); | |
| 87 | Label urlLabel = new Label(); | |
| 88 | urlField = new EscapeTextField(); | |
| 89 | Label textLabel = new Label(); | |
| 90 | textField = new EscapeTextField(); | |
| 91 | Label titleLabel = new Label(); | |
| 92 | titleField = new EscapeTextField(); | |
| 93 | ||
| 94 | //======== pane ======== | |
| 95 | { | |
| 96 | pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" ); | |
| 97 | pane.setRows( "[][][][]" ); | |
| 98 | ||
| 99 | //---- urlLabel ---- | |
| 100 | urlLabel.setText( get( "Dialog.link.urlLabel.text" ) ); | |
| 101 | pane.add( urlLabel, "cell 0 0" ); | |
| 102 | ||
| 103 | //---- urlField ---- | |
| 104 | urlField.setEscapeCharacters( "()" ); | |
| 105 | pane.add( urlField, "cell 1 0" ); | |
| 106 | ||
| 107 | //---- textLabel ---- | |
| 108 | textLabel.setText( get( "Dialog.link.textLabel.text" ) ); | |
| 109 | pane.add( textLabel, "cell 0 1" ); | |
| 110 | ||
| 111 | //---- textField ---- | |
| 112 | textField.setEscapeCharacters( "[]" ); | |
| 113 | pane.add( textField, "cell 1 1 3 1" ); | |
| 114 | ||
| 115 | //---- titleLabel ---- | |
| 116 | titleLabel.setText( get( "Dialog.link.titleLabel.text" ) ); | |
| 117 | pane.add( titleLabel, "cell 0 2" ); | |
| 118 | pane.add( titleField, "cell 1 2 3 1" ); | |
| 119 | } | |
| 120 | // JFormDesigner - End of component initialization //GEN-END:initComponents | |
| 121 | } | |
| 122 | ||
| 123 | // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables | |
| 124 | private MigPane pane; | |
| 125 | private EscapeTextField urlField; | |
| 126 | private EscapeTextField textField; | |
| 127 | private EscapeTextField titleField; | |
| 128 | // JFormDesigner - End of variables declaration //GEN-END:variables | |
| 129 | } | |
| 130 | 1 |
| 1 | JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8" | |
| 2 | ||
| 3 | new FormModel { | |
| 4 | "i18n.bundlePackage": "com.scrivendor" | |
| 5 | "i18n.bundleName": "messages" | |
| 6 | "i18n.autoExternalize": true | |
| 7 | "i18n.keyPrefix": "LinkDialog" | |
| 8 | contentType: "form/javafx" | |
| 9 | root: new FormRoot { | |
| 10 | add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) { | |
| 11 | "$layoutConstraints": "" | |
| 12 | "$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]" | |
| 13 | "$rowConstraints": "[][][][]" | |
| 14 | } ) { | |
| 15 | name: "pane" | |
| 16 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 17 | name: "urlLabel" | |
| 18 | "text": new FormMessage( null, "LinkDialog.urlLabel.text" ) | |
| 19 | auxiliary() { | |
| 20 | "JavaCodeGenerator.variableLocal": true | |
| 21 | } | |
| 22 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 23 | "value": "cell 0 0" | |
| 24 | } ) | |
| 25 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 26 | name: "urlField" | |
| 27 | "escapeCharacters": "()" | |
| 28 | "text": "http://yourlink.com" | |
| 29 | "promptText": "http://yourlink.com" | |
| 30 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 31 | "value": "cell 1 0" | |
| 32 | } ) | |
| 33 | add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) { | |
| 34 | name: "linkBrowseDirectoyButton" | |
| 35 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 36 | "value": "cell 2 0" | |
| 37 | } ) | |
| 38 | add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) { | |
| 39 | name: "linkBrowseFileButton" | |
| 40 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 41 | "value": "cell 3 0" | |
| 42 | } ) | |
| 43 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 44 | name: "textLabel" | |
| 45 | "text": new FormMessage( null, "LinkDialog.textLabel.text" ) | |
| 46 | auxiliary() { | |
| 47 | "JavaCodeGenerator.variableLocal": true | |
| 48 | } | |
| 49 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 50 | "value": "cell 0 1" | |
| 51 | } ) | |
| 52 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 53 | name: "textField" | |
| 54 | "escapeCharacters": "[]" | |
| 55 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 56 | "value": "cell 1 1 3 1" | |
| 57 | } ) | |
| 58 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 59 | name: "titleLabel" | |
| 60 | "text": new FormMessage( null, "LinkDialog.titleLabel.text" ) | |
| 61 | auxiliary() { | |
| 62 | "JavaCodeGenerator.variableLocal": true | |
| 63 | } | |
| 64 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 65 | "value": "cell 0 2" | |
| 66 | } ) | |
| 67 | add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) { | |
| 68 | name: "titleField" | |
| 69 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 70 | "value": "cell 1 2 3 1" | |
| 71 | } ) | |
| 72 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 73 | name: "previewLabel" | |
| 74 | "text": new FormMessage( null, "LinkDialog.previewLabel.text" ) | |
| 75 | auxiliary() { | |
| 76 | "JavaCodeGenerator.variableLocal": true | |
| 77 | } | |
| 78 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 79 | "value": "cell 0 3" | |
| 80 | } ) | |
| 81 | add( new FormComponent( "javafx.scene.control.Label" ) { | |
| 82 | name: "previewField" | |
| 83 | }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { | |
| 84 | "value": "cell 1 3 3 1" | |
| 85 | } ) | |
| 86 | }, new FormLayoutConstraints( null ) { | |
| 87 | "location": new javafx.geometry.Point2D( 0.0, 0.0 ) | |
| 88 | "size": new javafx.geometry.Dimension2D( 500.0, 300.0 ) | |
| 89 | } ) | |
| 90 | } | |
| 91 | } | |
| 92 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.editors; | |
| 29 | ||
| 30 | import com.scrivenvar.AbstractFileFactory; | |
| 31 | import com.scrivenvar.sigils.RSigilOperator; | |
| 32 | import com.scrivenvar.sigils.SigilOperator; | |
| 33 | import com.scrivenvar.sigils.YamlSigilOperator; | |
| 34 | ||
| 35 | import java.nio.file.Path; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for creating a definition name decorator suited to a particular | |
| 39 | * file type. | |
| 40 | */ | |
| 41 | public class DefinitionDecoratorFactory extends AbstractFileFactory { | |
| 42 | ||
| 43 | private DefinitionDecoratorFactory() { | |
| 44 | } | |
| 45 | ||
| 46 | public static SigilOperator newInstance( final Path path ) { | |
| 47 | final var factory = new DefinitionDecoratorFactory(); | |
| 48 | ||
| 49 | return switch( factory.lookup( path ) ) { | |
| 50 | case RMARKDOWN, RXML -> new RSigilOperator(); | |
| 51 | default -> new YamlSigilOperator(); | |
| 52 | }; | |
| 53 | } | |
| 54 | } | |
| 55 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.editors; | |
| 29 | ||
| 30 | import com.scrivenvar.FileEditorTab; | |
| 31 | import com.scrivenvar.definition.DefinitionPane; | |
| 32 | import com.scrivenvar.definition.DefinitionTreeItem; | |
| 33 | import com.scrivenvar.sigils.SigilOperator; | |
| 34 | import javafx.scene.control.TreeItem; | |
| 35 | import javafx.scene.input.KeyEvent; | |
| 36 | import org.fxmisc.richtext.StyledTextArea; | |
| 37 | ||
| 38 | import java.nio.file.Path; | |
| 39 | import java.text.BreakIterator; | |
| 40 | ||
| 41 | import static com.scrivenvar.Constants.*; | |
| 42 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 43 | import static java.lang.Character.isWhitespace; | |
| 44 | import static javafx.scene.input.KeyCode.SPACE; | |
| 45 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 46 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 47 | ||
| 48 | /** | |
| 49 | * Provides the logic for injecting variable names within the editor. | |
| 50 | */ | |
| 51 | public final class DefinitionNameInjector { | |
| 52 | ||
| 53 | /** | |
| 54 | * Recipient of name injections. | |
| 55 | */ | |
| 56 | private FileEditorTab mTab; | |
| 57 | ||
| 58 | /** | |
| 59 | * Initiates double-click events. | |
| 60 | */ | |
| 61 | private final DefinitionPane mDefinitionPane; | |
| 62 | ||
| 63 | /** | |
| 64 | * Initializes the variable name injector against the given pane. | |
| 65 | * | |
| 66 | * @param pane The definition panel to listen to for double-click events. | |
| 67 | */ | |
| 68 | public DefinitionNameInjector( final DefinitionPane pane ) { | |
| 69 | mDefinitionPane = pane; | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Trap Control+Space. | |
| 74 | * | |
| 75 | * @param tab Editor where variable names get injected. | |
| 76 | */ | |
| 77 | public void addListener( final FileEditorTab tab ) { | |
| 78 | assert tab != null; | |
| 79 | mTab = tab; | |
| 80 | ||
| 81 | tab.getEditorPane().addKeyboardListener( | |
| 82 | keyPressed( SPACE, CONTROL_DOWN ), | |
| 83 | this::autoinsert | |
| 84 | ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Inserts the currently selected variable from the {@link DefinitionPane}. | |
| 89 | */ | |
| 90 | public void injectSelectedItem() { | |
| 91 | final var pane = getDefinitionPane(); | |
| 92 | final TreeItem<String> item = pane.getSelectedItem(); | |
| 93 | ||
| 94 | if( item.isLeaf() ) { | |
| 95 | final var leaf = pane.findLeafExact( item.getValue() ); | |
| 96 | final var editor = getEditor(); | |
| 97 | ||
| 98 | editor.insertText( editor.getCaretPosition(), decorate( leaf ) ); | |
| 99 | } | |
| 100 | } | |
| 101 | ||
| 102 | /** | |
| 103 | * Pressing Control+SPACE will find a node that matches the current word and | |
| 104 | * substitute the definition reference. | |
| 105 | */ | |
| 106 | public void autoinsert() { | |
| 107 | final String paragraph = getCaretParagraph(); | |
| 108 | final int[] bounds = getWordBoundariesAtCaret(); | |
| 109 | ||
| 110 | try { | |
| 111 | if( isEmptyDefinitionPane() ) { | |
| 112 | alert( STATUS_DEFINITION_EMPTY ); | |
| 113 | } | |
| 114 | else { | |
| 115 | final String word = paragraph.substring( bounds[ 0 ], bounds[ 1 ] ); | |
| 116 | ||
| 117 | if( word.isBlank() ) { | |
| 118 | alert( STATUS_DEFINITION_BLANK ); | |
| 119 | } | |
| 120 | else { | |
| 121 | final var leaf = findLeaf( word ); | |
| 122 | ||
| 123 | if( leaf == null ) { | |
| 124 | alert( STATUS_DEFINITION_MISSING, word ); | |
| 125 | } | |
| 126 | else { | |
| 127 | replaceText( bounds[ 0 ], bounds[ 1 ], decorate( leaf ) ); | |
| 128 | expand( leaf ); | |
| 129 | } | |
| 130 | } | |
| 131 | } | |
| 132 | } catch( final Exception ignored ) { | |
| 133 | alert( STATUS_DEFINITION_BLANK ); | |
| 134 | } | |
| 135 | } | |
| 136 | ||
| 137 | /** | |
| 138 | * Pressing Control+SPACE will find a node that matches the current word and | |
| 139 | * substitute the definition reference. | |
| 140 | * | |
| 141 | * @param e Ignored -- it can only be Control+SPACE. | |
| 142 | */ | |
| 143 | private void autoinsert( final KeyEvent e ) { | |
| 144 | autoinsert(); | |
| 145 | } | |
| 146 | ||
| 147 | /** | |
| 148 | * Finds the start and end indexes for the word in the current paragraph | |
| 149 | * where the caret is located. There are a few different scenarios, where | |
| 150 | * the caret can be at: the start, end, or middle of a word; also, the | |
| 151 | * caret can be at the end or beginning of a punctuated word; as well, the | |
| 152 | * caret could be at the beginning or end of the line or document. | |
| 153 | */ | |
| 154 | private int[] getWordBoundariesAtCaret() { | |
| 155 | final var paragraph = getCaretParagraph(); | |
| 156 | final var length = paragraph.length(); | |
| 157 | int offset = getCurrentCaretColumn(); | |
| 158 | ||
| 159 | int began = offset; | |
| 160 | int ended = offset; | |
| 161 | ||
| 162 | while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) { | |
| 163 | began--; | |
| 164 | } | |
| 165 | ||
| 166 | while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) { | |
| 167 | ended++; | |
| 168 | } | |
| 169 | ||
| 170 | final var iterator = BreakIterator.getWordInstance(); | |
| 171 | iterator.setText( paragraph ); | |
| 172 | ||
| 173 | while( began < length && iterator.isBoundary( began + 1 ) ) { | |
| 174 | began++; | |
| 175 | } | |
| 176 | ||
| 177 | while( ended > 0 && iterator.isBoundary( ended - 1 ) ) { | |
| 178 | ended--; | |
| 179 | } | |
| 180 | ||
| 181 | return new int[]{began, ended}; | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * Decorates a {@link TreeItem} using the syntax specific to the type of | |
| 186 | * document being edited. | |
| 187 | * | |
| 188 | * @param leaf The path to the leaf (the definition key) to be decorated. | |
| 189 | */ | |
| 190 | private String decorate( final DefinitionTreeItem<String> leaf ) { | |
| 191 | return decorate( leaf.toPath() ); | |
| 192 | } | |
| 193 | ||
| 194 | /** | |
| 195 | * Decorates a variable using the syntax specific to the type of document | |
| 196 | * being edited. | |
| 197 | * | |
| 198 | * @param variable The variable to decorate in dot-notation without any | |
| 199 | * start or end sigils present. | |
| 200 | */ | |
| 201 | private String decorate( final String variable ) { | |
| 202 | return getVariableDecorator().apply( variable ); | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * Updates the text at the given position within the current paragraph. | |
| 207 | * | |
| 208 | * @param posBegan The starting index in the paragraph text to replace. | |
| 209 | * @param posEnded The ending index in the paragraph text to replace. | |
| 210 | * @param text Overwrite the paragraph substring with this text. | |
| 211 | */ | |
| 212 | private void replaceText( | |
| 213 | final int posBegan, final int posEnded, final String text ) { | |
| 214 | final int p = getCurrentParagraph(); | |
| 215 | ||
| 216 | getEditor().replaceText( p, posBegan, p, posEnded, text ); | |
| 217 | } | |
| 218 | ||
| 219 | /** | |
| 220 | * Returns the caret's current paragraph position. | |
| 221 | * | |
| 222 | * @return A number greater than or equal to 0. | |
| 223 | */ | |
| 224 | private int getCurrentParagraph() { | |
| 225 | return getEditor().getCurrentParagraph(); | |
| 226 | } | |
| 227 | ||
| 228 | /** | |
| 229 | * Returns the text for the paragraph that contains the caret. | |
| 230 | * | |
| 231 | * @return A non-null string, possibly empty. | |
| 232 | */ | |
| 233 | private String getCaretParagraph() { | |
| 234 | return getEditor().getText( getCurrentParagraph() ); | |
| 235 | } | |
| 236 | ||
| 237 | /** | |
| 238 | * Returns the caret position within the current paragraph. | |
| 239 | * | |
| 240 | * @return A value from 0 to the length of the current paragraph. | |
| 241 | */ | |
| 242 | private int getCurrentCaretColumn() { | |
| 243 | return getEditor().getCaretColumn(); | |
| 244 | } | |
| 245 | ||
| 246 | /** | |
| 247 | * Looks for the given word, matching first by exact, next by a starts-with | |
| 248 | * condition with diacritics replaced, then by containment. | |
| 249 | * | |
| 250 | * @param word The word to match by: exact, at the beginning, or containment. | |
| 251 | * @return The matching {@link DefinitionTreeItem} for the given word, or | |
| 252 | * {@code null} if none found. | |
| 253 | */ | |
| 254 | @SuppressWarnings("ConstantConditions") | |
| 255 | private DefinitionTreeItem<String> findLeaf( final String word ) { | |
| 256 | assert word != null; | |
| 257 | ||
| 258 | final var pane = getDefinitionPane(); | |
| 259 | DefinitionTreeItem<String> leaf = null; | |
| 260 | ||
| 261 | leaf = leaf == null ? pane.findLeafExact( word ) : leaf; | |
| 262 | leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf; | |
| 263 | leaf = leaf == null ? pane.findLeafContains( word ) : leaf; | |
| 264 | leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf; | |
| 265 | ||
| 266 | return leaf; | |
| 267 | } | |
| 268 | ||
| 269 | /** | |
| 270 | * Answers whether there are any definitions in the tree. | |
| 271 | * | |
| 272 | * @return {@code true} when there are no definitions; {@code false} when | |
| 273 | * there's at least one definition. | |
| 274 | */ | |
| 275 | private boolean isEmptyDefinitionPane() { | |
| 276 | return getDefinitionPane().isEmpty(); | |
| 277 | } | |
| 278 | ||
| 279 | /** | |
| 280 | * Collapses the tree then expands and selects the given node. | |
| 281 | * | |
| 282 | * @param node The node to expand. | |
| 283 | */ | |
| 284 | private void expand( final TreeItem<String> node ) { | |
| 285 | final DefinitionPane pane = getDefinitionPane(); | |
| 286 | pane.collapse(); | |
| 287 | pane.expand( node ); | |
| 288 | pane.select( node ); | |
| 289 | } | |
| 290 | ||
| 291 | /** | |
| 292 | * @return A variable decorator that corresponds to the given file type. | |
| 293 | */ | |
| 294 | private SigilOperator getVariableDecorator() { | |
| 295 | return DefinitionDecoratorFactory.newInstance( getFilename() ); | |
| 296 | } | |
| 297 | ||
| 298 | private Path getFilename() { | |
| 299 | return getFileEditorTab().getPath(); | |
| 300 | } | |
| 301 | ||
| 302 | private EditorPane getEditorPane() { | |
| 303 | return getFileEditorTab().getEditorPane(); | |
| 304 | } | |
| 305 | ||
| 306 | private StyledTextArea<?, ?> getEditor() { | |
| 307 | return getEditorPane().getEditor(); | |
| 308 | } | |
| 309 | ||
| 310 | public FileEditorTab getFileEditorTab() { | |
| 311 | return mTab; | |
| 312 | } | |
| 313 | ||
| 314 | private DefinitionPane getDefinitionPane() { | |
| 315 | return mDefinitionPane; | |
| 316 | } | |
| 317 | } | |
| 318 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.editors; | |
| 29 | ||
| 30 | import com.scrivenvar.preferences.UserPreferences; | |
| 31 | import javafx.beans.property.IntegerProperty; | |
| 32 | import javafx.beans.property.ObjectProperty; | |
| 33 | import javafx.beans.property.SimpleObjectProperty; | |
| 34 | import javafx.beans.value.ChangeListener; | |
| 35 | import javafx.event.Event; | |
| 36 | import javafx.scene.control.ScrollPane; | |
| 37 | import javafx.scene.layout.Pane; | |
| 38 | import org.fxmisc.flowless.VirtualizedScrollPane; | |
| 39 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 40 | import org.fxmisc.undo.UndoManager; | |
| 41 | import org.fxmisc.wellbehaved.event.EventPattern; | |
| 42 | import org.fxmisc.wellbehaved.event.Nodes; | |
| 43 | ||
| 44 | import java.nio.file.Path; | |
| 45 | import java.util.function.Consumer; | |
| 46 | ||
| 47 | import static com.scrivenvar.StatusBarNotifier.clearAlert; | |
| 48 | import static java.lang.String.format; | |
| 49 | import static javafx.application.Platform.runLater; | |
| 50 | import static org.fxmisc.wellbehaved.event.InputMap.consume; | |
| 51 | ||
| 52 | /** | |
| 53 | * Represents common editing features for various types of text editors. | |
| 54 | */ | |
| 55 | public class EditorPane extends Pane { | |
| 56 | ||
| 57 | /** | |
| 58 | * Used when changing the text area font size. | |
| 59 | */ | |
| 60 | private static final String FMT_CSS_FONT_SIZE = "-fx-font-size: %dpt;"; | |
| 61 | ||
| 62 | private final StyleClassedTextArea mEditor = | |
| 63 | new StyleClassedTextArea( false ); | |
| 64 | private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane = | |
| 65 | new VirtualizedScrollPane<>( mEditor ); | |
| 66 | private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>(); | |
| 67 | ||
| 68 | public EditorPane() { | |
| 69 | getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS ); | |
| 70 | fontsSizeProperty().addListener( | |
| 71 | ( l, o, n ) -> setFontSize( n.intValue() ) | |
| 72 | ); | |
| 73 | ||
| 74 | // Clear out any previous alerts after the user has typed. If the problem | |
| 75 | // persists, re-rendering the document will re-raise the error. If there | |
| 76 | // was no previous error, clearing the alert is essentially a no-op. | |
| 77 | mEditor.textProperty().addListener( | |
| 78 | ( l, o, n ) -> clearAlert() | |
| 79 | ); | |
| 80 | } | |
| 81 | ||
| 82 | @Override | |
| 83 | public void requestFocus() { | |
| 84 | requestFocus( 3 ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * There's a race-condition between displaying the {@link EditorPane} | |
| 89 | * and giving the {@link #mEditor} focus. Try to focus up to {@code max} | |
| 90 | * times before giving up. | |
| 91 | * | |
| 92 | * @param max The number of attempts to try to request focus. | |
| 93 | */ | |
| 94 | private void requestFocus( final int max ) { | |
| 95 | if( max > 0 ) { | |
| 96 | runLater( | |
| 97 | () -> { | |
| 98 | final var editor = getEditor(); | |
| 99 | ||
| 100 | if( !editor.isFocused() ) { | |
| 101 | editor.requestFocus(); | |
| 102 | requestFocus( max - 1 ); | |
| 103 | } | |
| 104 | } | |
| 105 | ); | |
| 106 | } | |
| 107 | } | |
| 108 | ||
| 109 | public void undo() { | |
| 110 | getUndoManager().undo(); | |
| 111 | } | |
| 112 | ||
| 113 | public void redo() { | |
| 114 | getUndoManager().redo(); | |
| 115 | } | |
| 116 | ||
| 117 | /** | |
| 118 | * Cuts the actively selected text; if no text is selected, this will cut | |
| 119 | * the entire paragraph. | |
| 120 | */ | |
| 121 | public void cut() { | |
| 122 | final var editor = getEditor(); | |
| 123 | final var selected = editor.getSelectedText(); | |
| 124 | ||
| 125 | if( selected == null || selected.isEmpty() ) { | |
| 126 | editor.selectParagraph(); | |
| 127 | } | |
| 128 | ||
| 129 | editor.cut(); | |
| 130 | } | |
| 131 | ||
| 132 | public void copy() { | |
| 133 | getEditor().copy(); | |
| 134 | } | |
| 135 | ||
| 136 | public void paste() { | |
| 137 | getEditor().paste(); | |
| 138 | } | |
| 139 | ||
| 140 | public void selectAll() { | |
| 141 | getEditor().selectAll(); | |
| 142 | } | |
| 143 | ||
| 144 | public UndoManager<?> getUndoManager() { | |
| 145 | return getEditor().getUndoManager(); | |
| 146 | } | |
| 147 | ||
| 148 | public String getText() { | |
| 149 | return getEditor().getText(); | |
| 150 | } | |
| 151 | ||
| 152 | public void setText( final String text ) { | |
| 153 | final var editor = getEditor(); | |
| 154 | editor.deselect(); | |
| 155 | editor.replaceText( text ); | |
| 156 | getUndoManager().mark(); | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * Call to hook into changes to the text area. | |
| 161 | * | |
| 162 | * @param listener Receives editor text change events. | |
| 163 | */ | |
| 164 | public void addTextChangeListener( | |
| 165 | final ChangeListener<? super String> listener ) { | |
| 166 | getEditor().textProperty().addListener( listener ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Notifies observers when the caret changes paragraph. | |
| 171 | * | |
| 172 | * @param listener Receives change event. | |
| 173 | */ | |
| 174 | public void addCaretParagraphListener( | |
| 175 | final ChangeListener<? super Integer> listener ) { | |
| 176 | getEditor().currentParagraphProperty().addListener( listener ); | |
| 177 | } | |
| 178 | ||
| 179 | /** | |
| 180 | * Notifies observers when the caret changes position. | |
| 181 | * | |
| 182 | * @param listener Receives change event. | |
| 183 | */ | |
| 184 | public void addCaretPositionListener( | |
| 185 | final ChangeListener<? super Integer> listener ) { | |
| 186 | getEditor().caretPositionProperty().addListener( listener ); | |
| 187 | } | |
| 188 | ||
| 189 | /** | |
| 190 | * This method adds listeners to editor events. | |
| 191 | * | |
| 192 | * @param <T> The event type. | |
| 193 | * @param <U> The consumer type for the given event type. | |
| 194 | * @param event The event of interest. | |
| 195 | * @param consumer The method to call when the event happens. | |
| 196 | */ | |
| 197 | public <T extends Event, U extends T> void addKeyboardListener( | |
| 198 | final EventPattern<? super T, ? extends U> event, | |
| 199 | final Consumer<? super U> consumer ) { | |
| 200 | Nodes.addInputMap( getEditor(), consume( event, consumer ) ); | |
| 201 | } | |
| 202 | ||
| 203 | /** | |
| 204 | * Repositions the cursor and scroll bar to the top of the file. | |
| 205 | */ | |
| 206 | public void scrollToTop() { | |
| 207 | getEditor().moveTo( 0 ); | |
| 208 | getScrollPane().scrollYToPixel( 0 ); | |
| 209 | } | |
| 210 | ||
| 211 | public StyleClassedTextArea getEditor() { | |
| 212 | return mEditor; | |
| 213 | } | |
| 214 | ||
| 215 | /** | |
| 216 | * Returns the scroll pane that contains the text area. | |
| 217 | * | |
| 218 | * @return The scroll pane that contains the content to edit. | |
| 219 | */ | |
| 220 | public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() { | |
| 221 | return mScrollPane; | |
| 222 | } | |
| 223 | ||
| 224 | public Path getPath() { | |
| 225 | return mPath.get(); | |
| 226 | } | |
| 227 | ||
| 228 | public void setPath( final Path path ) { | |
| 229 | mPath.set( path ); | |
| 230 | } | |
| 231 | ||
| 232 | /** | |
| 233 | * Sets the font size in points. | |
| 234 | * | |
| 235 | * @param size The new font size to use for the text editor. | |
| 236 | */ | |
| 237 | private void setFontSize( final int size ) { | |
| 238 | mEditor.setStyle( format( FMT_CSS_FONT_SIZE, size ) ); | |
| 239 | } | |
| 240 | ||
| 241 | /** | |
| 242 | * Returns the text editor font size property for handling font size change | |
| 243 | * events. | |
| 244 | */ | |
| 245 | private IntegerProperty fontsSizeProperty() { | |
| 246 | return UserPreferences.getInstance().fontsSizeEditorProperty(); | |
| 247 | } | |
| 248 | } | |
| 249 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.editors.markdown; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.ast.Link; | |
| 31 | ||
| 32 | /** | |
| 33 | * Represents the model for a hyperlink: text, url, and title. | |
| 34 | */ | |
| 35 | public class HyperlinkModel { | |
| 36 | ||
| 37 | private String text; | |
| 38 | private String url; | |
| 39 | private String title; | |
| 40 | ||
| 41 | /** | |
| 42 | * Constructs a new hyperlink model in Markdown format by default with no | |
| 43 | * title (i.e., tooltip). | |
| 44 | * | |
| 45 | * @param text The hyperlink text displayed (e.g., displayed to the user). | |
| 46 | * @param url The destination URL (e.g., when clicked). | |
| 47 | */ | |
| 48 | public HyperlinkModel( final String text, final String url ) { | |
| 49 | this( text, url, null ); | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Constructs a new hyperlink model for the given AST link. | |
| 54 | * | |
| 55 | * @param link A markdown link. | |
| 56 | */ | |
| 57 | public HyperlinkModel( final Link link ) { | |
| 58 | this( | |
| 59 | link.getText().toString(), | |
| 60 | link.getUrl().toString(), | |
| 61 | link.getTitle().toString() | |
| 62 | ); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Constructs a new hyperlink model in Markdown format by default. | |
| 67 | * | |
| 68 | * @param text The hyperlink text displayed (e.g., displayed to the user). | |
| 69 | * @param url The destination URL (e.g., when clicked). | |
| 70 | * @param title The hyperlink title (e.g., shown as a tooltip). | |
| 71 | */ | |
| 72 | public HyperlinkModel( final String text, final String url, | |
| 73 | final String title ) { | |
| 74 | setText( text ); | |
| 75 | setUrl( url ); | |
| 76 | setTitle( title ); | |
| 77 | } | |
| 78 | ||
| 79 | /** | |
| 80 | * Returns the string in Markdown format by default. | |
| 81 | * | |
| 82 | * @return A markdown version of the hyperlink. | |
| 83 | */ | |
| 84 | @Override | |
| 85 | public String toString() { | |
| 86 | String format = "%s%s%s"; | |
| 87 | ||
| 88 | if( hasText() ) { | |
| 89 | format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)"); | |
| 90 | } | |
| 91 | ||
| 92 | // Becomes ""+URL+"" if no text is set. | |
| 93 | // Becomes [TITLE]+(URL)+"" if no title is set. | |
| 94 | // Becomes [TITLE]+(URL+ \"TITLE\") if title is set. | |
| 95 | return String.format( format, getText(), getUrl(), getTitle() ); | |
| 96 | } | |
| 97 | ||
| 98 | public final void setText( final String text ) { | |
| 99 | this.text = nullSafe( text ); | |
| 100 | } | |
| 101 | ||
| 102 | public final void setUrl( final String url ) { | |
| 103 | this.url = nullSafe( url ); | |
| 104 | } | |
| 105 | ||
| 106 | public final void setTitle( final String title ) { | |
| 107 | this.title = nullSafe( title ); | |
| 108 | } | |
| 109 | ||
| 110 | /** | |
| 111 | * Answers whether text has been set for the hyperlink. | |
| 112 | * | |
| 113 | * @return true This is a text link. | |
| 114 | */ | |
| 115 | public boolean hasText() { | |
| 116 | return !getText().isEmpty(); | |
| 117 | } | |
| 118 | ||
| 119 | /** | |
| 120 | * Answers whether a title (tooltip) has been set for the hyperlink. | |
| 121 | * | |
| 122 | * @return true There is a title. | |
| 123 | */ | |
| 124 | public boolean hasTitle() { | |
| 125 | return !getTitle().isEmpty(); | |
| 126 | } | |
| 127 | ||
| 128 | public String getText() { | |
| 129 | return this.text; | |
| 130 | } | |
| 131 | ||
| 132 | public String getUrl() { | |
| 133 | return this.url; | |
| 134 | } | |
| 135 | ||
| 136 | public String getTitle() { | |
| 137 | return this.title; | |
| 138 | } | |
| 139 | ||
| 140 | private String nullSafe( final String s ) { | |
| 141 | return s == null ? "" : s; | |
| 142 | } | |
| 143 | } | |
| 144 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.editors.markdown; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.ast.Link; | |
| 31 | import com.vladsch.flexmark.util.ast.Node; | |
| 32 | import com.vladsch.flexmark.util.ast.NodeVisitor; | |
| 33 | import com.vladsch.flexmark.util.ast.VisitHandler; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for extracting a hyperlink from the document so that the user | |
| 37 | * can edit the link within a dialog. | |
| 38 | */ | |
| 39 | public class LinkVisitor { | |
| 40 | ||
| 41 | private NodeVisitor visitor; | |
| 42 | private Link link; | |
| 43 | private final int offset; | |
| 44 | ||
| 45 | /** | |
| 46 | * Creates a hyperlink given an offset into a paragraph and the markdown AST | |
| 47 | * link node. | |
| 48 | * | |
| 49 | * @param index Index into the paragraph that indicates the hyperlink to | |
| 50 | * change. | |
| 51 | */ | |
| 52 | public LinkVisitor( final int index ) { | |
| 53 | this.offset = index; | |
| 54 | } | |
| 55 | ||
| 56 | public Link process( final Node root ) { | |
| 57 | getVisitor().visit( root ); | |
| 58 | return getLink(); | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * @param link Not null. | |
| 63 | */ | |
| 64 | private void visit( final Link link ) { | |
| 65 | final int began = link.getStartOffset(); | |
| 66 | final int ended = link.getEndOffset(); | |
| 67 | final int index = getOffset(); | |
| 68 | ||
| 69 | if( index >= began && index <= ended ) { | |
| 70 | setLink( link ); | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | private synchronized NodeVisitor getVisitor() { | |
| 75 | if( this.visitor == null ) { | |
| 76 | this.visitor = createVisitor(); | |
| 77 | } | |
| 78 | ||
| 79 | return this.visitor; | |
| 80 | } | |
| 81 | ||
| 82 | protected NodeVisitor createVisitor() { | |
| 83 | return new NodeVisitor( | |
| 84 | new VisitHandler<>( Link.class, LinkVisitor.this::visit ) ); | |
| 85 | } | |
| 86 | ||
| 87 | private Link getLink() { | |
| 88 | return this.link; | |
| 89 | } | |
| 90 | ||
| 91 | private void setLink( final Link link ) { | |
| 92 | this.link = link; | |
| 93 | } | |
| 94 | ||
| 95 | public int getOffset() { | |
| 96 | return this.offset; | |
| 97 | } | |
| 98 | } | |
| 99 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.editors.markdown; | |
| 29 | ||
| 30 | import com.scrivenvar.dialogs.ImageDialog; | |
| 31 | import com.scrivenvar.dialogs.LinkDialog; | |
| 32 | import com.scrivenvar.editors.EditorPane; | |
| 33 | import com.scrivenvar.processors.markdown.BlockExtension; | |
| 34 | import com.scrivenvar.processors.markdown.MarkdownProcessor; | |
| 35 | import com.vladsch.flexmark.ast.Link; | |
| 36 | import com.vladsch.flexmark.html.renderer.AttributablePart; | |
| 37 | import com.vladsch.flexmark.util.ast.Node; | |
| 38 | import com.vladsch.flexmark.util.html.MutableAttributes; | |
| 39 | import javafx.scene.control.Dialog; | |
| 40 | import javafx.scene.control.IndexRange; | |
| 41 | import javafx.scene.input.KeyCode; | |
| 42 | import javafx.scene.input.KeyEvent; | |
| 43 | import javafx.stage.Window; | |
| 44 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 45 | ||
| 46 | import java.nio.file.Path; | |
| 47 | import java.util.ArrayList; | |
| 48 | import java.util.List; | |
| 49 | import java.util.regex.Matcher; | |
| 50 | import java.util.regex.Pattern; | |
| 51 | ||
| 52 | import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN; | |
| 53 | import static com.scrivenvar.util.Utils.ltrim; | |
| 54 | import static com.scrivenvar.util.Utils.rtrim; | |
| 55 | import static javafx.scene.input.KeyCode.ENTER; | |
| 56 | import static javafx.scene.input.KeyCombination.CONTROL_DOWN; | |
| 57 | import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed; | |
| 58 | ||
| 59 | /** | |
| 60 | * Provides the ability to edit a text document. | |
| 61 | */ | |
| 62 | public class MarkdownEditorPane extends EditorPane { | |
| 63 | private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile( | |
| 64 | "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" ); | |
| 65 | ||
| 66 | /** | |
| 67 | * Any of these followed by a space and a letter produce a line | |
| 68 | * by themselves. The ">" need not be followed by a space. | |
| 69 | */ | |
| 70 | private static final Pattern PATTERN_NEW_LINE = Pattern.compile( | |
| 71 | "^>|(((#+)|([*+\\-])|([1-9]\\.))\\s+).+" ); | |
| 72 | ||
| 73 | public MarkdownEditorPane() { | |
| 74 | initEditor(); | |
| 75 | } | |
| 76 | ||
| 77 | private void initEditor() { | |
| 78 | final StyleClassedTextArea textArea = getEditor(); | |
| 79 | ||
| 80 | textArea.setWrapText( true ); | |
| 81 | textArea.getStyleClass().add( "markdown-editor" ); | |
| 82 | textArea.getStylesheets().add( STYLESHEET_MARKDOWN ); | |
| 83 | ||
| 84 | addKeyboardListener( keyPressed( ENTER ), this::enterPressed ); | |
| 85 | addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut ); | |
| 86 | } | |
| 87 | ||
| 88 | public void insertLink() { | |
| 89 | insertObject( createLinkDialog() ); | |
| 90 | } | |
| 91 | ||
| 92 | public void insertImage() { | |
| 93 | insertObject( createImageDialog() ); | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Returns the editor's paragraph number that will be close to its HTML | |
| 98 | * paragraph ID. Ultimately this solution is flawed because there isn't | |
| 99 | * a straightforward correlation between the document being edited and | |
| 100 | * what is rendered. XML documents transformed through stylesheets have | |
| 101 | * no readily determined correlation. Images, tables, and other | |
| 102 | * objects affect the relative location of the current paragraph being | |
| 103 | * edited with respect to the preview pane. | |
| 104 | * <p> | |
| 105 | * See | |
| 106 | * {@link BlockExtension.IdAttributeProvider#setAttributes(Node, AttributablePart, MutableAttributes)}} | |
| 107 | * for details. | |
| 108 | * </p> | |
| 109 | * <p> | |
| 110 | * Injecting a token into the document, as per a previous version of the | |
| 111 | * application, can instruct the preview pane where to shift the viewport. | |
| 112 | * </p> | |
| 113 | * | |
| 114 | * @param paraIndex The paragraph index from the editor pane to scroll to | |
| 115 | * in the preview pane, which will be approximated if an | |
| 116 | * equivalent cannot be found. | |
| 117 | * @return A unique identifier that correlates to an equivalent paragraph | |
| 118 | * number once the Markdown is rendered into HTML. | |
| 119 | */ | |
| 120 | public int approximateParagraphId( final int paraIndex ) { | |
| 121 | final StyleClassedTextArea editor = getEditor(); | |
| 122 | final List<String> lines = new ArrayList<>( 4096 ); | |
| 123 | ||
| 124 | int i = 0; | |
| 125 | String prevText = ""; | |
| 126 | boolean withinFencedBlock = false; | |
| 127 | boolean withinCodeBlock = false; | |
| 128 | ||
| 129 | for( final var p : editor.getParagraphs() ) { | |
| 130 | if( i > paraIndex ) { | |
| 131 | break; | |
| 132 | } | |
| 133 | ||
| 134 | final String text = p.getText().replace( '>', ' ' ); | |
| 135 | if( text.startsWith( "```" ) ) { | |
| 136 | if( withinFencedBlock = !withinFencedBlock ) { | |
| 137 | lines.add( text ); | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | if( !withinFencedBlock ) { | |
| 142 | final boolean foundCodeBlock = text.startsWith( " " ); | |
| 143 | ||
| 144 | if( foundCodeBlock && !withinCodeBlock ) { | |
| 145 | lines.add( text ); | |
| 146 | withinCodeBlock = true; | |
| 147 | } | |
| 148 | else if( !foundCodeBlock ) { | |
| 149 | withinCodeBlock = false; | |
| 150 | } | |
| 151 | } | |
| 152 | ||
| 153 | if( !withinFencedBlock && !withinCodeBlock && | |
| 154 | ((!text.isBlank() && prevText.isBlank()) || | |
| 155 | PATTERN_NEW_LINE.matcher( text ).matches()) ) { | |
| 156 | lines.add( text ); | |
| 157 | } | |
| 158 | ||
| 159 | prevText = text; | |
| 160 | i++; | |
| 161 | } | |
| 162 | ||
| 163 | // Scrolling index is 1-based. | |
| 164 | return Math.max( lines.size() - 1, 0 ); | |
| 165 | } | |
| 166 | ||
| 167 | /** | |
| 168 | * Gets the index of the paragraph where the caret is positioned. | |
| 169 | * | |
| 170 | * @return The paragraph number for the caret. | |
| 171 | */ | |
| 172 | public int getCurrentParagraphIndex() { | |
| 173 | return getEditor().getCurrentParagraph(); | |
| 174 | } | |
| 175 | ||
| 176 | /** | |
| 177 | * @param leading Characters to insert at the beginning of the current | |
| 178 | * selection (or paragraph). | |
| 179 | * @param trailing Characters to insert at the end of the current selection | |
| 180 | * (or paragraph). | |
| 181 | */ | |
| 182 | public void surroundSelection( final String leading, final String trailing ) { | |
| 183 | surroundSelection( leading, trailing, null ); | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * @param leading Characters to insert at the beginning of the current | |
| 188 | * selection (or paragraph). | |
| 189 | * @param trailing Characters to insert at the end of the current selection | |
| 190 | * (or paragraph). | |
| 191 | * @param hint Instructional text inserted within the leading and | |
| 192 | * trailing characters, provided no text is selected. | |
| 193 | */ | |
| 194 | public void surroundSelection( | |
| 195 | String leading, String trailing, final String hint ) { | |
| 196 | final StyleClassedTextArea textArea = getEditor(); | |
| 197 | ||
| 198 | // Note: not using textArea.insertText() to insert leading and trailing | |
| 199 | // because this would add two changes to undo history | |
| 200 | final IndexRange selection = textArea.getSelection(); | |
| 201 | int start = selection.getStart(); | |
| 202 | int end = selection.getEnd(); | |
| 203 | ||
| 204 | final String selectedText = textArea.getSelectedText(); | |
| 205 | ||
| 206 | String trimmedText = selectedText.trim(); | |
| 207 | if( trimmedText.length() < selectedText.length() ) { | |
| 208 | start += selectedText.indexOf( trimmedText ); | |
| 209 | end = start + trimmedText.length(); | |
| 210 | } | |
| 211 | ||
| 212 | // remove leading whitespaces from leading text if selection starts at zero | |
| 213 | if( start == 0 ) { | |
| 214 | leading = ltrim( leading ); | |
| 215 | } | |
| 216 | ||
| 217 | // remove trailing whitespaces from trailing text if selection ends at | |
| 218 | // text end | |
| 219 | if( end == textArea.getLength() ) { | |
| 220 | trailing = rtrim( trailing ); | |
| 221 | } | |
| 222 | ||
| 223 | // remove leading line separators from leading text | |
| 224 | // if there are line separators before the selected text | |
| 225 | if( leading.startsWith( "\n" ) ) { | |
| 226 | for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) { | |
| 227 | if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) { | |
| 228 | break; | |
| 229 | } | |
| 230 | ||
| 231 | leading = leading.substring( 1 ); | |
| 232 | } | |
| 233 | } | |
| 234 | ||
| 235 | // remove trailing line separators from trailing or leading text | |
| 236 | // if there are line separators after the selected text | |
| 237 | final boolean trailingIsEmpty = trailing.isEmpty(); | |
| 238 | String str = trailingIsEmpty ? leading : trailing; | |
| 239 | ||
| 240 | if( str.endsWith( "\n" ) ) { | |
| 241 | final int length = textArea.getLength(); | |
| 242 | ||
| 243 | for( int i = end; i < length && str.endsWith( "\n" ); i++ ) { | |
| 244 | if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) { | |
| 245 | break; | |
| 246 | } | |
| 247 | ||
| 248 | str = str.substring( 0, str.length() - 1 ); | |
| 249 | } | |
| 250 | ||
| 251 | if( trailingIsEmpty ) { | |
| 252 | leading = str; | |
| 253 | } | |
| 254 | else { | |
| 255 | trailing = str; | |
| 256 | } | |
| 257 | } | |
| 258 | ||
| 259 | int selStart = start + leading.length(); | |
| 260 | int selEnd = end + leading.length(); | |
| 261 | ||
| 262 | // insert hint text if selection is empty | |
| 263 | if( hint != null && trimmedText.isEmpty() ) { | |
| 264 | trimmedText = hint; | |
| 265 | selEnd = selStart + hint.length(); | |
| 266 | } | |
| 267 | ||
| 268 | // prevent undo merging with previous text entered by user | |
| 269 | getUndoManager().preventMerge(); | |
| 270 | ||
| 271 | // replace text and update selection | |
| 272 | textArea.replaceText( start, end, leading + trimmedText + trailing ); | |
| 273 | textArea.selectRange( selStart, selEnd ); | |
| 274 | } | |
| 275 | ||
| 276 | private void enterPressed( final KeyEvent e ) { | |
| 277 | final StyleClassedTextArea textArea = getEditor(); | |
| 278 | final String currentLine = | |
| 279 | textArea.getText( textArea.getCurrentParagraph() ); | |
| 280 | final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine ); | |
| 281 | ||
| 282 | String newText = "\n"; | |
| 283 | ||
| 284 | if( matcher.matches() ) { | |
| 285 | if( !matcher.group( 2 ).isEmpty() ) { | |
| 286 | // indent new line with same whitespace characters and list markers | |
| 287 | // as current line | |
| 288 | newText = newText.concat( matcher.group( 1 ) ); | |
| 289 | } | |
| 290 | else { | |
| 291 | // current line contains only whitespace characters and list markers | |
| 292 | // --> empty current line | |
| 293 | final int caretPosition = textArea.getCaretPosition(); | |
| 294 | textArea.selectRange( caretPosition - currentLine.length(), | |
| 295 | caretPosition ); | |
| 296 | } | |
| 297 | } | |
| 298 | ||
| 299 | textArea.replaceSelection( newText ); | |
| 300 | ||
| 301 | // Ensure that the window scrolls when Enter is pressed at the bottom of | |
| 302 | // the pane. | |
| 303 | textArea.requestFollowCaret(); | |
| 304 | } | |
| 305 | ||
| 306 | private void cut( final KeyEvent event ) { | |
| 307 | super.cut(); | |
| 308 | } | |
| 309 | ||
| 310 | /** | |
| 311 | * Returns one of: selected text, word under cursor, or parsed hyperlink from | |
| 312 | * the markdown AST. | |
| 313 | * | |
| 314 | * @return An instance containing the link URL and display text. | |
| 315 | */ | |
| 316 | private HyperlinkModel getHyperlink() { | |
| 317 | final StyleClassedTextArea textArea = getEditor(); | |
| 318 | final String selectedText = textArea.getSelectedText(); | |
| 319 | ||
| 320 | // Get the current paragraph, convert to Markdown nodes. | |
| 321 | final MarkdownProcessor mp = new MarkdownProcessor( null ); | |
| 322 | final int p = textArea.getCurrentParagraph(); | |
| 323 | final String paragraph = textArea.getText( p ); | |
| 324 | final Node node = mp.toNode( paragraph ); | |
| 325 | final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() ); | |
| 326 | final Link link = visitor.process( node ); | |
| 327 | ||
| 328 | if( link != null ) { | |
| 329 | textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() ); | |
| 330 | } | |
| 331 | ||
| 332 | return createHyperlinkModel( | |
| 333 | link, selectedText, "https://localhost" | |
| 334 | ); | |
| 335 | } | |
| 336 | ||
| 337 | @SuppressWarnings("SameParameterValue") | |
| 338 | private HyperlinkModel createHyperlinkModel( | |
| 339 | final Link link, final String selection, final String url ) { | |
| 340 | ||
| 341 | return link == null | |
| 342 | ? new HyperlinkModel( selection, url ) | |
| 343 | : new HyperlinkModel( link ); | |
| 344 | } | |
| 345 | ||
| 346 | private Path getParentPath() { | |
| 347 | final Path path = getPath(); | |
| 348 | return (path != null) ? path.getParent() : null; | |
| 349 | } | |
| 350 | ||
| 351 | private Dialog<String> createLinkDialog() { | |
| 352 | return new LinkDialog( getWindow(), getHyperlink() ); | |
| 353 | } | |
| 354 | ||
| 355 | private Dialog<String> createImageDialog() { | |
| 356 | return new ImageDialog( getWindow(), getParentPath() ); | |
| 357 | } | |
| 358 | ||
| 359 | private void insertObject( final Dialog<String> dialog ) { | |
| 360 | dialog.showAndWait().ifPresent( | |
| 361 | result -> getEditor().replaceSelection( result ) | |
| 362 | ); | |
| 363 | } | |
| 364 | ||
| 365 | private Window getWindow() { | |
| 366 | return getScrollPane().getScene().getWindow(); | |
| 367 | } | |
| 368 | } | |
| 369 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.predicates; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.util.Collection; | |
| 32 | import java.util.function.Predicate; | |
| 33 | ||
| 34 | import static java.lang.String.join; | |
| 35 | import static java.nio.file.FileSystems.getDefault; | |
| 36 | ||
| 37 | /** | |
| 38 | * Provides a number of simple {@link Predicate} instances for various types | |
| 39 | * of string comparisons, including basic strings and file name strings. | |
| 40 | */ | |
| 41 | public class PredicateFactory { | |
| 42 | /** | |
| 43 | * Creates an instance of {@link Predicate} that matches a globbed file | |
| 44 | * name pattern. | |
| 45 | * | |
| 46 | * @param pattern The file name pattern to match. | |
| 47 | * @return A {@link Predicate} that can answer whether a given file name | |
| 48 | * matches the given glob pattern. | |
| 49 | */ | |
| 50 | public static Predicate<File> createFileTypePredicate( | |
| 51 | final String pattern ) { | |
| 52 | final var matcher = getDefault().getPathMatcher( | |
| 53 | "glob:**{" + pattern + "}" | |
| 54 | ); | |
| 55 | ||
| 56 | return file -> matcher.matches( file.toPath() ); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Creates an instance of {@link Predicate} that matches any file name from | |
| 61 | * a {@link Collection} of file name patterns. The given patterns are joined | |
| 62 | * with commas into a single comma-separated list. | |
| 63 | * | |
| 64 | * @param patterns The file name patterns to be matched. | |
| 65 | * @return A {@link Predicate} that can answer whether a given file name | |
| 66 | * matches the given glob patterns. | |
| 67 | */ | |
| 68 | public static Predicate<File> createFileTypePredicate( | |
| 69 | final Collection<String> patterns ) { | |
| 70 | return createFileTypePredicate( join( ",", patterns ) ); | |
| 71 | } | |
| 72 | ||
| 73 | /** | |
| 74 | * Creates an instance of {@link Predicate} that compares whether the given | |
| 75 | * {@code reference} string is contained by the comparator. Comparison is | |
| 76 | * case-insensitive. The test will also pass if the comparate is empty. | |
| 77 | * | |
| 78 | * @param comparator The string to check as being contained. | |
| 79 | * @return A {@link Predicate} that can answer whether the given string | |
| 80 | * is contained within the comparator, or the comparate is empty. | |
| 81 | */ | |
| 82 | public static Predicate<String> createStringContainsPredicate( | |
| 83 | final String comparator ) { | |
| 84 | return comparate -> comparate.isEmpty() || | |
| 85 | comparate.toLowerCase().contains( comparator.toLowerCase() ); | |
| 86 | } | |
| 87 | 1 | |
| 88 | /** | |
| 89 | * Creates an instance of {@link Predicate} that compares whether the given | |
| 90 | * {@code reference} string is starts with the comparator. Comparison is | |
| 91 | * case-insensitive. | |
| 92 | * | |
| 93 | * @param comparator The string to check as being contained. | |
| 94 | * @return A {@link Predicate} that can answer whether the given string | |
| 95 | * is contained within the comparator. | |
| 96 | */ | |
| 97 | public static Predicate<String> createStringStartsPredicate( | |
| 98 | final String comparator ) { | |
| 99 | return comparate -> | |
| 100 | comparate.toLowerCase().startsWith( comparator.toLowerCase() ); | |
| 101 | } | |
| 102 | } |
| 1 | /* | |
| 2 | * Copyright 2016 David Croft and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preferences; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.io.FileInputStream; | |
| 32 | import java.io.FileOutputStream; | |
| 33 | import java.util.*; | |
| 34 | import java.util.prefs.AbstractPreferences; | |
| 35 | import java.util.prefs.BackingStoreException; | |
| 36 | ||
| 37 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 38 | ||
| 39 | /** | |
| 40 | * Preferences implementation that stores to a user-defined file. Local file | |
| 41 | * storage is preferred over a certain operating system's monolithic trash heap | |
| 42 | * called a registry. When the OS is locked down, the default Preferences | |
| 43 | * implementation will try to write to the registry and fail due to permissions | |
| 44 | * problems. This class sidesteps the issue entirely by writing to the user's | |
| 45 | * home directory, where permissions should be a bit more lax. | |
| 46 | */ | |
| 47 | public class FilePreferences extends AbstractPreferences { | |
| 48 | ||
| 49 | private final Map<String, String> mRoot = new TreeMap<>(); | |
| 50 | private final Map<String, FilePreferences> mChildren = new TreeMap<>(); | |
| 51 | private boolean mRemoved; | |
| 52 | ||
| 53 | private final Object mMutex = new Object(); | |
| 54 | ||
| 55 | public FilePreferences( | |
| 56 | final AbstractPreferences parent, final String name ) { | |
| 57 | super( parent, name ); | |
| 58 | ||
| 59 | try { | |
| 60 | sync(); | |
| 61 | } catch( final BackingStoreException ex ) { | |
| 62 | alert( ex ); | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | @Override | |
| 67 | protected void putSpi( final String key, final String value ) { | |
| 68 | synchronized( mMutex ) { | |
| 69 | mRoot.put( key, value ); | |
| 70 | } | |
| 71 | ||
| 72 | try { | |
| 73 | flush(); | |
| 74 | } catch( final BackingStoreException ex ) { | |
| 75 | alert( ex ); | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | @Override | |
| 80 | protected String getSpi( final String key ) { | |
| 81 | synchronized( mMutex ) { | |
| 82 | return mRoot.get( key ); | |
| 83 | } | |
| 84 | } | |
| 85 | ||
| 86 | @Override | |
| 87 | protected void removeSpi( final String key ) { | |
| 88 | synchronized( mMutex ) { | |
| 89 | mRoot.remove( key ); | |
| 90 | } | |
| 91 | ||
| 92 | try { | |
| 93 | flush(); | |
| 94 | } catch( final BackingStoreException ex ) { | |
| 95 | alert( ex ); | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | @Override | |
| 100 | protected void removeNodeSpi() throws BackingStoreException { | |
| 101 | mRemoved = true; | |
| 102 | flush(); | |
| 103 | } | |
| 104 | ||
| 105 | @Override | |
| 106 | protected String[] keysSpi() { | |
| 107 | synchronized( mMutex ) { | |
| 108 | return mRoot.keySet().toArray( new String[ 0 ] ); | |
| 109 | } | |
| 110 | } | |
| 111 | ||
| 112 | @Override | |
| 113 | protected String[] childrenNamesSpi() { | |
| 114 | return mChildren.keySet().toArray( new String[ 0 ] ); | |
| 115 | } | |
| 116 | ||
| 117 | @Override | |
| 118 | protected FilePreferences childSpi( final String name ) { | |
| 119 | FilePreferences child = mChildren.get( name ); | |
| 120 | ||
| 121 | if( child == null || child.isRemoved() ) { | |
| 122 | child = new FilePreferences( this, name ); | |
| 123 | mChildren.put( name, child ); | |
| 124 | } | |
| 125 | ||
| 126 | return child; | |
| 127 | } | |
| 128 | ||
| 129 | @Override | |
| 130 | protected void syncSpi() { | |
| 131 | if( isRemoved() ) { | |
| 132 | return; | |
| 133 | } | |
| 134 | ||
| 135 | final File file = FilePreferencesFactory.getPreferencesFile(); | |
| 136 | ||
| 137 | if( !file.exists() ) { | |
| 138 | return; | |
| 139 | } | |
| 140 | ||
| 141 | synchronized( mMutex ) { | |
| 142 | final Properties p = new Properties(); | |
| 143 | ||
| 144 | try( final var inputStream = new FileInputStream( file ) ) { | |
| 145 | p.load( inputStream ); | |
| 146 | ||
| 147 | final String path = getPath(); | |
| 148 | final Enumeration<?> propertyNames = p.propertyNames(); | |
| 149 | ||
| 150 | while( propertyNames.hasMoreElements() ) { | |
| 151 | final String propKey = (String) propertyNames.nextElement(); | |
| 152 | ||
| 153 | if( propKey.startsWith( path ) ) { | |
| 154 | final String subKey = propKey.substring( path.length() ); | |
| 155 | ||
| 156 | // Only load immediate descendants | |
| 157 | if( subKey.indexOf( '.' ) == -1 ) { | |
| 158 | mRoot.put( subKey, p.getProperty( propKey ) ); | |
| 159 | } | |
| 160 | } | |
| 161 | } | |
| 162 | } catch( final Exception ex ) { | |
| 163 | alert( ex ); | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | ||
| 168 | private String getPath() { | |
| 169 | final FilePreferences parent = (FilePreferences) parent(); | |
| 170 | ||
| 171 | return parent == null ? "" : parent.getPath() + name() + '.'; | |
| 172 | } | |
| 173 | ||
| 174 | @Override | |
| 175 | protected void flushSpi() { | |
| 176 | final File file = FilePreferencesFactory.getPreferencesFile(); | |
| 177 | ||
| 178 | synchronized( mMutex ) { | |
| 179 | final Properties p = new Properties(); | |
| 180 | ||
| 181 | try { | |
| 182 | final String path = getPath(); | |
| 183 | ||
| 184 | if( file.exists() ) { | |
| 185 | try( final var fis = new FileInputStream( file ) ) { | |
| 186 | p.load( fis ); | |
| 187 | } | |
| 188 | ||
| 189 | final List<String> toRemove = new ArrayList<>(); | |
| 190 | ||
| 191 | // Make a list of all direct children of this node to be removed | |
| 192 | final Enumeration<?> propertyNames = p.propertyNames(); | |
| 193 | ||
| 194 | while( propertyNames.hasMoreElements() ) { | |
| 195 | final String propKey = (String) propertyNames.nextElement(); | |
| 196 | if( propKey.startsWith( path ) ) { | |
| 197 | final String subKey = propKey.substring( path.length() ); | |
| 198 | ||
| 199 | // Only do immediate descendants | |
| 200 | if( subKey.indexOf( '.' ) == -1 ) { | |
| 201 | toRemove.add( propKey ); | |
| 202 | } | |
| 203 | } | |
| 204 | } | |
| 205 | ||
| 206 | // Remove them now that the enumeration is done with | |
| 207 | for( final String propKey : toRemove ) { | |
| 208 | p.remove( propKey ); | |
| 209 | } | |
| 210 | } | |
| 211 | ||
| 212 | // If this node hasn't been removed, add back in any values | |
| 213 | if( !mRemoved ) { | |
| 214 | for( final String s : mRoot.keySet() ) { | |
| 215 | p.setProperty( path + s, mRoot.get( s ) ); | |
| 216 | } | |
| 217 | } | |
| 218 | ||
| 219 | try( final var fos = new FileOutputStream( file ) ) { | |
| 220 | p.store( fos, "FilePreferences" ); | |
| 221 | } | |
| 222 | } catch( final Exception ex ) { | |
| 223 | alert( ex ); | |
| 224 | } | |
| 225 | } | |
| 226 | } | |
| 227 | } | |
| 228 | 1 |
| 1 | /* | |
| 2 | * Copyright 2016 David Croft and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preferences; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.nio.file.FileSystems; | |
| 32 | import java.util.prefs.Preferences; | |
| 33 | import java.util.prefs.PreferencesFactory; | |
| 34 | ||
| 35 | import static com.scrivenvar.Constants.APP_TITLE; | |
| 36 | ||
| 37 | /** | |
| 38 | * PreferencesFactory implementation that stores the preferences in a | |
| 39 | * user-defined file. Usage: | |
| 40 | * <pre> | |
| 41 | * System.setProperty( "java.util.prefs.PreferencesFactory", | |
| 42 | * FilePreferencesFactory.class.getName() ); | |
| 43 | * </pre> | |
| 44 | * <p> | |
| 45 | * The file defaults to <code>$user.home/.scrivenvar</code>, but can be changed | |
| 46 | * using <code>-Dapplication.name=preferences</code> when running the | |
| 47 | * application, or by calling <code>System.setProperty</code> with the | |
| 48 | * "application.name" property. | |
| 49 | * </p> | |
| 50 | */ | |
| 51 | public class FilePreferencesFactory implements PreferencesFactory { | |
| 52 | ||
| 53 | private static File preferencesFile; | |
| 54 | private Preferences rootPreferences; | |
| 55 | ||
| 56 | @Override | |
| 57 | public Preferences systemRoot() { | |
| 58 | return userRoot(); | |
| 59 | } | |
| 60 | ||
| 61 | @Override | |
| 62 | public synchronized Preferences userRoot() { | |
| 63 | if( rootPreferences == null ) { | |
| 64 | rootPreferences = new FilePreferences( null, "" ); | |
| 65 | } | |
| 66 | ||
| 67 | return rootPreferences; | |
| 68 | } | |
| 69 | ||
| 70 | public synchronized static File getPreferencesFile() { | |
| 71 | if( preferencesFile == null ) { | |
| 72 | String prefsFile = getPreferencesFilename(); | |
| 73 | ||
| 74 | preferencesFile = new File( prefsFile ).getAbsoluteFile(); | |
| 75 | } | |
| 76 | ||
| 77 | return preferencesFile; | |
| 78 | } | |
| 79 | ||
| 80 | public static String getPreferencesFilename() { | |
| 81 | final String filename = System.getProperty( "application.name", APP_TITLE ); | |
| 82 | return System.getProperty( "user.home" ) + getSeparator() + "." + filename; | |
| 83 | } | |
| 84 | ||
| 85 | public static String getSeparator() { | |
| 86 | return FileSystems.getDefault().getSeparator(); | |
| 87 | } | |
| 88 | } | |
| 89 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preferences; | |
| 29 | ||
| 30 | import com.dlsc.formsfx.model.structure.StringField; | |
| 31 | import com.dlsc.preferencesfx.PreferencesFx; | |
| 32 | import com.dlsc.preferencesfx.PreferencesFxEvent; | |
| 33 | import com.dlsc.preferencesfx.model.Category; | |
| 34 | import com.dlsc.preferencesfx.model.Group; | |
| 35 | import com.dlsc.preferencesfx.model.Setting; | |
| 36 | import javafx.beans.property.*; | |
| 37 | import javafx.event.EventHandler; | |
| 38 | import javafx.scene.Node; | |
| 39 | import javafx.scene.control.Label; | |
| 40 | ||
| 41 | import java.io.File; | |
| 42 | import java.nio.file.Path; | |
| 43 | ||
| 44 | import static com.scrivenvar.Constants.*; | |
| 45 | import static com.scrivenvar.Messages.get; | |
| 46 | ||
| 47 | /** | |
| 48 | * Responsible for user preferences that can be changed from the GUI. The | |
| 49 | * settings are displayed and persisted using {@link PreferencesFx}. | |
| 50 | */ | |
| 51 | public class UserPreferences { | |
| 52 | /** | |
| 53 | * Implementation of the initialization-on-demand holder design pattern, | |
| 54 | * an for a lazy-loaded singleton. In all versions of Java, the idiom enables | |
| 55 | * a safe, highly concurrent lazy initialization of static fields with good | |
| 56 | * performance. The implementation relies upon the initialization phase of | |
| 57 | * execution within the Java Virtual Machine (JVM) as specified by the Java | |
| 58 | * Language Specification. When the class {@link UserPreferencesContainer} | |
| 59 | * is loaded, its initialization completes trivially because there are no | |
| 60 | * static variables to initialize. | |
| 61 | * <p> | |
| 62 | * The static class definition {@link UserPreferencesContainer} within the | |
| 63 | * {@link UserPreferences} is not initialized until such time that | |
| 64 | * {@link UserPreferencesContainer} must be executed. The static | |
| 65 | * {@link UserPreferencesContainer} class executes when | |
| 66 | * {@link #getInstance} is called. The first call will trigger loading and | |
| 67 | * initialization of the {@link UserPreferencesContainer} thereby | |
| 68 | * instantiating the {@link #INSTANCE}. | |
| 69 | * </p> | |
| 70 | * <p> | |
| 71 | * This indirection is necessary because the {@link UserPreferences} class | |
| 72 | * references {@link PreferencesFx}, which must not be instantiated until the | |
| 73 | * UI is ready. | |
| 74 | * </p> | |
| 75 | */ | |
| 76 | private static class UserPreferencesContainer { | |
| 77 | private static final UserPreferences INSTANCE = new UserPreferences(); | |
| 78 | } | |
| 79 | ||
| 80 | public static UserPreferences getInstance() { | |
| 81 | return UserPreferencesContainer.INSTANCE; | |
| 82 | } | |
| 83 | ||
| 84 | private final PreferencesFx mPreferencesFx; | |
| 85 | ||
| 86 | private final ObjectProperty<File> mPropRDirectory; | |
| 87 | private final StringProperty mPropRScript; | |
| 88 | private final ObjectProperty<File> mPropImagesDirectory; | |
| 89 | private final StringProperty mPropImagesOrder; | |
| 90 | private final ObjectProperty<File> mPropDefinitionPath; | |
| 91 | private final StringProperty mRDelimiterBegan; | |
| 92 | private final StringProperty mRDelimiterEnded; | |
| 93 | private final StringProperty mDefDelimiterBegan; | |
| 94 | private final StringProperty mDefDelimiterEnded; | |
| 95 | private final IntegerProperty mPropFontsSizeEditor; | |
| 96 | ||
| 97 | private UserPreferences() { | |
| 98 | mPropRDirectory = simpleFile( USER_DIRECTORY ); | |
| 99 | mPropRScript = new SimpleStringProperty( "" ); | |
| 100 | ||
| 101 | mPropImagesDirectory = simpleFile( USER_DIRECTORY ); | |
| 102 | mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT ); | |
| 103 | ||
| 104 | mPropDefinitionPath = simpleFile( | |
| 105 | getSetting( "file.definition.default", DEFINITION_NAME ) | |
| 106 | ); | |
| 107 | ||
| 108 | mDefDelimiterBegan = new SimpleStringProperty( DEF_DELIM_BEGAN_DEFAULT ); | |
| 109 | mDefDelimiterEnded = new SimpleStringProperty( DEF_DELIM_ENDED_DEFAULT ); | |
| 110 | ||
| 111 | mRDelimiterBegan = new SimpleStringProperty( R_DELIM_BEGAN_DEFAULT ); | |
| 112 | mRDelimiterEnded = new SimpleStringProperty( R_DELIM_ENDED_DEFAULT ); | |
| 113 | ||
| 114 | mPropFontsSizeEditor = new SimpleIntegerProperty( (int) FONT_SIZE_EDITOR ); | |
| 115 | ||
| 116 | // All properties must be initialized before creating the dialog. | |
| 117 | mPreferencesFx = createPreferencesFx(); | |
| 118 | } | |
| 119 | ||
| 120 | /** | |
| 121 | * Display the user preferences settings dialog (non-modal). | |
| 122 | */ | |
| 123 | public void show() { | |
| 124 | getPreferencesFx().show( false ); | |
| 125 | } | |
| 126 | ||
| 127 | /** | |
| 128 | * Call to persist the settings. Strictly speaking, this could watch on | |
| 129 | * all values for external changes then save automatically. | |
| 130 | */ | |
| 131 | public void save() { | |
| 132 | getPreferencesFx().saveSettings(); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Creates the preferences dialog. | |
| 137 | * <p> | |
| 138 | * TODO: Make this dynamic by iterating over all "Preferences.*" values | |
| 139 | * that follow a particular naming pattern. | |
| 140 | * </p> | |
| 141 | * | |
| 142 | * @return A new instance of preferences for users to edit. | |
| 143 | */ | |
| 144 | @SuppressWarnings("unchecked") | |
| 145 | private PreferencesFx createPreferencesFx() { | |
| 146 | final Setting<StringField, StringProperty> scriptSetting = | |
| 147 | Setting.of( "Script", mPropRScript ); | |
| 148 | final StringField field = scriptSetting.getElement(); | |
| 149 | field.multiline( true ); | |
| 150 | ||
| 151 | return PreferencesFx.of( | |
| 152 | UserPreferences.class, | |
| 153 | Category.of( | |
| 154 | get( "Preferences.r" ), | |
| 155 | Group.of( | |
| 156 | get( "Preferences.r.directory" ), | |
| 157 | Setting.of( label( "Preferences.r.directory.desc", false ) ), | |
| 158 | Setting.of( "Directory", mPropRDirectory, true ) | |
| 159 | ), | |
| 160 | Group.of( | |
| 161 | get( "Preferences.r.script" ), | |
| 162 | Setting.of( label( "Preferences.r.script.desc" ) ), | |
| 163 | scriptSetting | |
| 164 | ), | |
| 165 | Group.of( | |
| 166 | get( "Preferences.r.delimiter.began" ), | |
| 167 | Setting.of( label( "Preferences.r.delimiter.began.desc" ) ), | |
| 168 | Setting.of( "Opening", mRDelimiterBegan ) | |
| 169 | ), | |
| 170 | Group.of( | |
| 171 | get( "Preferences.r.delimiter.ended" ), | |
| 172 | Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ), | |
| 173 | Setting.of( "Closing", mRDelimiterEnded ) | |
| 174 | ) | |
| 175 | ), | |
| 176 | Category.of( | |
| 177 | get( "Preferences.images" ), | |
| 178 | Group.of( | |
| 179 | get( "Preferences.images.directory" ), | |
| 180 | Setting.of( label( "Preferences.images.directory.desc" ) ), | |
| 181 | Setting.of( "Directory", mPropImagesDirectory, true ) | |
| 182 | ), | |
| 183 | Group.of( | |
| 184 | get( "Preferences.images.suffixes" ), | |
| 185 | Setting.of( label( "Preferences.images.suffixes.desc" ) ), | |
| 186 | Setting.of( "Extensions", mPropImagesOrder ) | |
| 187 | ) | |
| 188 | ), | |
| 189 | Category.of( | |
| 190 | get( "Preferences.definitions" ), | |
| 191 | Group.of( | |
| 192 | get( "Preferences.definitions.path" ), | |
| 193 | Setting.of( label( "Preferences.definitions.path.desc" ) ), | |
| 194 | Setting.of( "Path", mPropDefinitionPath, false ) | |
| 195 | ), | |
| 196 | Group.of( | |
| 197 | get( "Preferences.definitions.delimiter.began" ), | |
| 198 | Setting.of( label( | |
| 199 | "Preferences.definitions.delimiter.began.desc" ) ), | |
| 200 | Setting.of( "Opening", mDefDelimiterBegan ) | |
| 201 | ), | |
| 202 | Group.of( | |
| 203 | get( "Preferences.definitions.delimiter.ended" ), | |
| 204 | Setting.of( label( | |
| 205 | "Preferences.definitions.delimiter.ended.desc" ) ), | |
| 206 | Setting.of( "Closing", mDefDelimiterEnded ) | |
| 207 | ) | |
| 208 | ), | |
| 209 | Category.of( | |
| 210 | get( "Preferences.fonts" ), | |
| 211 | Group.of( | |
| 212 | get( "Preferences.fonts.size_editor" ), | |
| 213 | Setting.of( label( "Preferences.fonts.size_editor.desc" ) ), | |
| 214 | Setting.of( "Points", mPropFontsSizeEditor ) | |
| 215 | ) | |
| 216 | ) | |
| 217 | ).instantPersistent( false ); | |
| 218 | } | |
| 219 | ||
| 220 | /** | |
| 221 | * Wraps a {@link File} inside a {@link SimpleObjectProperty}. | |
| 222 | * | |
| 223 | * @param path The file name to use when constructing the {@link File}. | |
| 224 | * @return A new {@link SimpleObjectProperty} instance with a {@link File} | |
| 225 | * that references the given {@code path}. | |
| 226 | */ | |
| 227 | private SimpleObjectProperty<File> simpleFile( final String path ) { | |
| 228 | return new SimpleObjectProperty<>( new File( path ) ); | |
| 229 | } | |
| 230 | ||
| 231 | /** | |
| 232 | * Creates a label for the given key after interpolating its value. | |
| 233 | * | |
| 234 | * @param key The key to find in the resource bundle. | |
| 235 | * @return The value of the key as a label. | |
| 236 | */ | |
| 237 | private Node label( final String key ) { | |
| 238 | return new Label( get( key, true ) ); | |
| 239 | } | |
| 240 | ||
| 241 | /** | |
| 242 | * Creates a label for the given key. | |
| 243 | * | |
| 244 | * @param key The key to find in the resource bundle. | |
| 245 | * @param interpolate {@code true} means to interpolate the value. | |
| 246 | * @return The value of the key, interpolated if {@code interpolate} is | |
| 247 | * {@code true}. | |
| 248 | */ | |
| 249 | @SuppressWarnings("SameParameterValue") | |
| 250 | private Node label( final String key, final boolean interpolate ) { | |
| 251 | return new Label( get( key, interpolate ) ); | |
| 252 | } | |
| 253 | ||
| 254 | /** | |
| 255 | * Delegates to the {@link PreferencesFx} event handler for monitoring | |
| 256 | * save events. | |
| 257 | * | |
| 258 | * @param eventHandler The handler to call when the preferences are saved. | |
| 259 | */ | |
| 260 | public void addSaveEventHandler( | |
| 261 | final EventHandler<? super PreferencesFxEvent> eventHandler ) { | |
| 262 | final var eventType = PreferencesFxEvent.EVENT_PREFERENCES_SAVED; | |
| 263 | getPreferencesFx().addEventHandler( eventType, eventHandler ); | |
| 264 | } | |
| 265 | ||
| 266 | /** | |
| 267 | * Returns the value for a key from the settings properties file. | |
| 268 | * | |
| 269 | * @param key Key within the settings properties file to find. | |
| 270 | * @param value Default value to return if the key is not found. | |
| 271 | * @return The value for the given key from the settings file, or the | |
| 272 | * given {@code value} if no key found. | |
| 273 | */ | |
| 274 | @SuppressWarnings("SameParameterValue") | |
| 275 | private String getSetting( final String key, final String value ) { | |
| 276 | return SETTINGS.getSetting( key, value ); | |
| 277 | } | |
| 278 | ||
| 279 | public ObjectProperty<File> definitionPathProperty() { | |
| 280 | return mPropDefinitionPath; | |
| 281 | } | |
| 282 | ||
| 283 | public Path getDefinitionPath() { | |
| 284 | return definitionPathProperty().getValue().toPath(); | |
| 285 | } | |
| 286 | ||
| 287 | private StringProperty defDelimiterBegan() { | |
| 288 | return mDefDelimiterBegan; | |
| 289 | } | |
| 290 | ||
| 291 | public String getDefDelimiterBegan() { | |
| 292 | return defDelimiterBegan().get(); | |
| 293 | } | |
| 294 | ||
| 295 | private StringProperty defDelimiterEnded() { | |
| 296 | return mDefDelimiterEnded; | |
| 297 | } | |
| 298 | ||
| 299 | public String getDefDelimiterEnded() { | |
| 300 | return defDelimiterEnded().get(); | |
| 301 | } | |
| 302 | ||
| 303 | public ObjectProperty<File> rDirectoryProperty() { | |
| 304 | return mPropRDirectory; | |
| 305 | } | |
| 306 | ||
| 307 | public File getRDirectory() { | |
| 308 | return rDirectoryProperty().getValue(); | |
| 309 | } | |
| 310 | ||
| 311 | public StringProperty rScriptProperty() { | |
| 312 | return mPropRScript; | |
| 313 | } | |
| 314 | ||
| 315 | public String getRScript() { | |
| 316 | return rScriptProperty().getValue(); | |
| 317 | } | |
| 318 | ||
| 319 | private StringProperty rDelimiterBegan() { | |
| 320 | return mRDelimiterBegan; | |
| 321 | } | |
| 322 | ||
| 323 | public String getRDelimiterBegan() { | |
| 324 | return rDelimiterBegan().get(); | |
| 325 | } | |
| 326 | ||
| 327 | private StringProperty rDelimiterEnded() { | |
| 328 | return mRDelimiterEnded; | |
| 329 | } | |
| 330 | ||
| 331 | public String getRDelimiterEnded() { | |
| 332 | return rDelimiterEnded().get(); | |
| 333 | } | |
| 334 | ||
| 335 | private ObjectProperty<File> imagesDirectoryProperty() { | |
| 336 | return mPropImagesDirectory; | |
| 337 | } | |
| 338 | ||
| 339 | public File getImagesDirectory() { | |
| 340 | return imagesDirectoryProperty().getValue(); | |
| 341 | } | |
| 342 | ||
| 343 | private StringProperty imagesOrderProperty() { | |
| 344 | return mPropImagesOrder; | |
| 345 | } | |
| 346 | ||
| 347 | public String getImagesOrder() { | |
| 348 | return imagesOrderProperty().getValue(); | |
| 349 | } | |
| 350 | ||
| 351 | public IntegerProperty fontsSizeEditorProperty() { | |
| 352 | return mPropFontsSizeEditor; | |
| 353 | } | |
| 354 | ||
| 355 | /** | |
| 356 | * Returns the preferred font size of the text editor. | |
| 357 | * | |
| 358 | * @return A non-negative integer, in points. | |
| 359 | */ | |
| 360 | public int getFontsSizeEditor() { | |
| 361 | return mPropFontsSizeEditor.intValue(); | |
| 362 | } | |
| 363 | ||
| 364 | private PreferencesFx getPreferencesFx() { | |
| 365 | return mPreferencesFx; | |
| 366 | } | |
| 367 | } | |
| 368 | 1 |
| 1 | /* | |
| 2 | * Copyright 2006 Patrick Wright | |
| 3 | * Copyright 2007 Wisconsin Court System | |
| 4 | * Copyright 2020 White Magic Software, Ltd. | |
| 5 | * | |
| 6 | * This program is free software; you can redistribute it and/or | |
| 7 | * modify it under the terms of the GNU Lesser General Public License | |
| 8 | * as published by the Free Software Foundation; either version 2.1 | |
| 9 | * of the License, or (at your option) any later version. | |
| 10 | * | |
| 11 | * This program is distributed in the hope that it will be useful, | |
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 14 | * GNU Lesser General Public License for more details. | |
| 15 | * | |
| 16 | * You should have received a copy of the GNU Lesser General Public License | |
| 17 | * along with this program; if not, write to the Free Software | |
| 18 | * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
| 19 | */ | |
| 20 | package com.scrivenvar.preview; | |
| 21 | ||
| 22 | import com.scrivenvar.adapters.ReplacedElementAdapter; | |
| 23 | import org.w3c.dom.Element; | |
| 24 | import org.xhtmlrenderer.extend.ReplacedElement; | |
| 25 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 26 | import org.xhtmlrenderer.extend.UserAgentCallback; | |
| 27 | import org.xhtmlrenderer.layout.LayoutContext; | |
| 28 | import org.xhtmlrenderer.render.BlockBox; | |
| 29 | ||
| 30 | import java.util.HashSet; | |
| 31 | import java.util.Set; | |
| 32 | ||
| 33 | public class ChainedReplacedElementFactory extends ReplacedElementAdapter { | |
| 34 | private final Set<ReplacedElementFactory> mFactoryList = new HashSet<>(); | |
| 35 | ||
| 36 | @Override | |
| 37 | public ReplacedElement createReplacedElement( | |
| 38 | final LayoutContext c, | |
| 39 | final BlockBox box, | |
| 40 | final UserAgentCallback uac, | |
| 41 | final int cssWidth, | |
| 42 | final int cssHeight ) { | |
| 43 | for( final var f : mFactoryList ) { | |
| 44 | final var r = f.createReplacedElement( | |
| 45 | c, box, uac, cssWidth, cssHeight ); | |
| 46 | ||
| 47 | if( r != null ) { | |
| 48 | return r; | |
| 49 | } | |
| 50 | } | |
| 51 | ||
| 52 | return null; | |
| 53 | } | |
| 54 | ||
| 55 | @Override | |
| 56 | public void reset() { | |
| 57 | for( final var factory : mFactoryList ) { | |
| 58 | factory.reset(); | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | @Override | |
| 63 | public void remove( final Element element ) { | |
| 64 | for( final var factory : mFactoryList ) { | |
| 65 | factory.remove( element ); | |
| 66 | } | |
| 67 | } | |
| 68 | ||
| 69 | public void addFactory( final ReplacedElementFactory factory ) { | |
| 70 | mFactoryList.add( factory ); | |
| 71 | } | |
| 72 | } | |
| 73 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preview; | |
| 29 | ||
| 30 | import javafx.beans.property.IntegerProperty; | |
| 31 | import javafx.beans.property.SimpleIntegerProperty; | |
| 32 | import org.xhtmlrenderer.extend.FSImage; | |
| 33 | import org.xhtmlrenderer.resource.ImageResource; | |
| 34 | import org.xhtmlrenderer.swing.ImageResourceLoader; | |
| 35 | ||
| 36 | import javax.imageio.ImageIO; | |
| 37 | import java.net.URI; | |
| 38 | import java.net.URL; | |
| 39 | import java.nio.file.Paths; | |
| 40 | ||
| 41 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 42 | import static com.scrivenvar.preview.SvgRasterizer.BROKEN_IMAGE_PLACEHOLDER; | |
| 43 | import static com.scrivenvar.util.ProtocolResolver.getProtocol; | |
| 44 | import static java.lang.String.valueOf; | |
| 45 | import static java.nio.file.Files.exists; | |
| 46 | import static org.xhtmlrenderer.swing.AWTFSImage.createImage; | |
| 47 | ||
| 48 | /** | |
| 49 | * Responsible for loading images. If the image cannot be found, a placeholder | |
| 50 | * is used instead. | |
| 51 | */ | |
| 52 | public class CustomImageLoader extends ImageResourceLoader { | |
| 53 | /** | |
| 54 | * Placeholder that's displayed when image cannot be found. | |
| 55 | */ | |
| 56 | private FSImage mBrokenImage; | |
| 57 | ||
| 58 | private final IntegerProperty mWidthProperty = new SimpleIntegerProperty(); | |
| 59 | ||
| 60 | /** | |
| 61 | * Gets an {@link IntegerProperty} that represents the maximum width an | |
| 62 | * image should be scaled. | |
| 63 | * | |
| 64 | * @return The maximum width for an image. | |
| 65 | */ | |
| 66 | public IntegerProperty widthProperty() { | |
| 67 | return mWidthProperty; | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Gets an image resolved from the given URI. If the image cannot be found, | |
| 72 | * this will return a custom placeholder image indicating the reference | |
| 73 | * is broken. | |
| 74 | * | |
| 75 | * @param uri Path to the image resource to load. | |
| 76 | * @param width Ignored. | |
| 77 | * @param height Ignored. | |
| 78 | * @return The scaled image, or a placeholder image if the URI's content | |
| 79 | * could not be retrieved. | |
| 80 | */ | |
| 81 | @Override | |
| 82 | public synchronized ImageResource get( | |
| 83 | final String uri, final int width, final int height ) { | |
| 84 | assert uri != null; | |
| 85 | assert width >= 0; | |
| 86 | assert height >= 0; | |
| 87 | ||
| 88 | try { | |
| 89 | final var protocol = getProtocol( uri ); | |
| 90 | final ImageResource imageResource; | |
| 91 | ||
| 92 | if( protocol.isFile() && exists( Paths.get( new URI( uri ) ) ) ) { | |
| 93 | imageResource = super.get( uri, width, height ); | |
| 94 | } | |
| 95 | else if( protocol.isHttp() ) { | |
| 96 | // FlyingSaucer will silently swallow any images that fail to load. | |
| 97 | // Consequently, the following lines load the resource over HTTP and | |
| 98 | // translate errors into a broken image icon. | |
| 99 | final var url = new URL( uri ); | |
| 100 | final var image = ImageIO.read( url ); | |
| 101 | imageResource = new ImageResource( uri, createImage( image ) ); | |
| 102 | } | |
| 103 | else { | |
| 104 | // Caught below to return a broken image; exception is swallowed. | |
| 105 | throw new UnsupportedOperationException( valueOf( protocol ) ); | |
| 106 | } | |
| 107 | ||
| 108 | return scale( imageResource ); | |
| 109 | } catch( final Exception e ) { | |
| 110 | alert( e ); | |
| 111 | return new ImageResource( uri, getBrokenImage() ); | |
| 112 | } | |
| 113 | } | |
| 114 | ||
| 115 | /** | |
| 116 | * Scales the image found at the given URI. | |
| 117 | * | |
| 118 | * @param ir {@link ImageResource} of image loaded successfully. | |
| 119 | * @return Resource representing the rendered image and path. | |
| 120 | */ | |
| 121 | private ImageResource scale( final ImageResource ir ) { | |
| 122 | final var image = ir.getImage(); | |
| 123 | final var imageWidth = image.getWidth(); | |
| 124 | final var imageHeight = image.getHeight(); | |
| 125 | ||
| 126 | int maxWidth = mWidthProperty.get(); | |
| 127 | int newWidth = imageWidth; | |
| 128 | int newHeight = imageHeight; | |
| 129 | ||
| 130 | // Maintain aspect ratio while shrinking image to view port bounds. | |
| 131 | if( imageWidth > maxWidth ) { | |
| 132 | newWidth = maxWidth; | |
| 133 | newHeight = (newWidth * imageHeight) / imageWidth; | |
| 134 | } | |
| 135 | ||
| 136 | image.scale( newWidth, newHeight ); | |
| 137 | return ir; | |
| 138 | } | |
| 139 | ||
| 140 | /** | |
| 141 | * Lazily initializes the broken image placeholder. | |
| 142 | * | |
| 143 | * @return The {@link FSImage} that represents a broken image icon. | |
| 144 | */ | |
| 145 | private FSImage getBrokenImage() { | |
| 146 | final var image = mBrokenImage; | |
| 147 | ||
| 148 | if( image == null ) { | |
| 149 | mBrokenImage = createImage( BROKEN_IMAGE_PLACEHOLDER ); | |
| 150 | } | |
| 151 | ||
| 152 | return mBrokenImage; | |
| 153 | } | |
| 154 | } | |
| 155 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preview; | |
| 29 | ||
| 30 | import com.scrivenvar.adapters.DocumentAdapter; | |
| 31 | import javafx.beans.property.BooleanProperty; | |
| 32 | import javafx.beans.property.SimpleBooleanProperty; | |
| 33 | import javafx.beans.value.ChangeListener; | |
| 34 | import javafx.beans.value.ObservableValue; | |
| 35 | import javafx.embed.swing.SwingNode; | |
| 36 | import javafx.scene.Node; | |
| 37 | import org.jsoup.Jsoup; | |
| 38 | import org.jsoup.helper.W3CDom; | |
| 39 | import org.jsoup.nodes.Document; | |
| 40 | import org.xhtmlrenderer.layout.SharedContext; | |
| 41 | import org.xhtmlrenderer.render.Box; | |
| 42 | import org.xhtmlrenderer.simple.XHTMLPanel; | |
| 43 | import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler; | |
| 44 | import org.xhtmlrenderer.swing.*; | |
| 45 | ||
| 46 | import javax.swing.*; | |
| 47 | import java.awt.*; | |
| 48 | import java.awt.event.ComponentAdapter; | |
| 49 | import java.awt.event.ComponentEvent; | |
| 50 | import java.net.URI; | |
| 51 | import java.nio.file.Path; | |
| 52 | ||
| 53 | import static com.scrivenvar.Constants.*; | |
| 54 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 55 | import static com.scrivenvar.util.ProtocolResolver.getProtocol; | |
| 56 | import static java.awt.Desktop.Action.BROWSE; | |
| 57 | import static java.awt.Desktop.getDesktop; | |
| 58 | import static java.lang.Math.max; | |
| 59 | import static javax.swing.SwingUtilities.invokeLater; | |
| 60 | import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER; | |
| 61 | ||
| 62 | /** | |
| 63 | * HTML preview pane is responsible for rendering an HTML document. | |
| 64 | */ | |
| 65 | public final class HTMLPreviewPane extends SwingNode { | |
| 66 | ||
| 67 | /** | |
| 68 | * Suppresses scrolling to the top on every key press. | |
| 69 | */ | |
| 70 | private static class HTMLPanel extends XHTMLPanel { | |
| 71 | @Override | |
| 72 | public void resetScrollPosition() { | |
| 73 | } | |
| 74 | } | |
| 75 | ||
| 76 | /** | |
| 77 | * Suppresses scroll attempts until after the document has loaded. | |
| 78 | */ | |
| 79 | private static final class DocumentEventHandler extends DocumentAdapter { | |
| 80 | private final BooleanProperty mReadyProperty = new SimpleBooleanProperty(); | |
| 81 | ||
| 82 | public BooleanProperty readyProperty() { | |
| 83 | return mReadyProperty; | |
| 84 | } | |
| 85 | ||
| 86 | @Override | |
| 87 | public void documentStarted() { | |
| 88 | mReadyProperty.setValue( Boolean.FALSE ); | |
| 89 | } | |
| 90 | ||
| 91 | @Override | |
| 92 | public void documentLoaded() { | |
| 93 | mReadyProperty.setValue( Boolean.TRUE ); | |
| 94 | } | |
| 95 | } | |
| 96 | ||
| 97 | /** | |
| 98 | * Ensure that images are constrained to the panel width upon resizing. | |
| 99 | */ | |
| 100 | private final class ResizeListener extends ComponentAdapter { | |
| 101 | @Override | |
| 102 | public void componentResized( final ComponentEvent e ) { | |
| 103 | setWidth( e ); | |
| 104 | } | |
| 105 | ||
| 106 | @Override | |
| 107 | public void componentShown( final ComponentEvent e ) { | |
| 108 | setWidth( e ); | |
| 109 | } | |
| 110 | ||
| 111 | /** | |
| 112 | * Sets the width of the {@link HTMLPreviewPane} so that images can be | |
| 113 | * scaled to fit. The scale factor is adjusted a bit below the full width | |
| 114 | * to prevent the horizontal scrollbar from appearing. | |
| 115 | * | |
| 116 | * @param event The component that defines the image scaling width. | |
| 117 | */ | |
| 118 | private void setWidth( final ComponentEvent event ) { | |
| 119 | final int width = (int) (event.getComponent().getWidth() * .95); | |
| 120 | HTMLPreviewPane.this.mImageLoader.widthProperty().set( width ); | |
| 121 | } | |
| 122 | } | |
| 123 | ||
| 124 | /** | |
| 125 | * Responsible for opening hyperlinks. External hyperlinks are opened in | |
| 126 | * the system's default browser; local file system links are opened in the | |
| 127 | * editor. | |
| 128 | */ | |
| 129 | private static class HyperlinkListener extends LinkListener { | |
| 130 | @Override | |
| 131 | public void linkClicked( final BasicPanel panel, final String link ) { | |
| 132 | try { | |
| 133 | final var protocol = getProtocol( link ); | |
| 134 | ||
| 135 | switch( protocol ) { | |
| 136 | case HTTP: | |
| 137 | final var desktop = getDesktop(); | |
| 138 | ||
| 139 | if( desktop.isSupported( BROWSE ) ) { | |
| 140 | desktop.browse( new URI( link ) ); | |
| 141 | } | |
| 142 | break; | |
| 143 | case FILE: | |
| 144 | // TODO: #88 -- publish a message to the event bus. | |
| 145 | break; | |
| 146 | } | |
| 147 | } catch( final Exception ex ) { | |
| 148 | alert( ex ); | |
| 149 | } | |
| 150 | } | |
| 151 | } | |
| 152 | ||
| 153 | /** | |
| 154 | * The CSS must be rendered in points (pt) not pixels (px) to avoid blurry | |
| 155 | * rendering on some platforms. | |
| 156 | */ | |
| 157 | private static final String HTML_PREFIX = "<!DOCTYPE html>" | |
| 158 | + "<html>" | |
| 159 | + "<head>" | |
| 160 | + "<link rel='stylesheet' href='" + | |
| 161 | HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) + "'/>" | |
| 162 | + "</head>" | |
| 163 | + "<body>"; | |
| 164 | ||
| 165 | // Provide some extra space at the end for scrolling past the last line. | |
| 166 | private static final String HTML_SUFFIX = | |
| 167 | "<p style='height=2em'> </p></body></html>"; | |
| 168 | ||
| 169 | private static final W3CDom W3C_DOM = new W3CDom(); | |
| 170 | private static final XhtmlNamespaceHandler NS_HANDLER = | |
| 171 | new XhtmlNamespaceHandler(); | |
| 172 | ||
| 173 | private final StringBuilder mHtmlDocument = new StringBuilder( 65536 ); | |
| 174 | private final int mHtmlPrefixLength; | |
| 175 | ||
| 176 | private final HTMLPanel mHtmlRenderer = new HTMLPanel(); | |
| 177 | private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer ); | |
| 178 | private final DocumentEventHandler mDocHandler = new DocumentEventHandler(); | |
| 179 | private final CustomImageLoader mImageLoader = new CustomImageLoader(); | |
| 180 | ||
| 181 | private Path mPath = DEFAULT_DIRECTORY; | |
| 182 | ||
| 183 | /** | |
| 184 | * Creates a new preview pane that can scroll to the caret position within the | |
| 185 | * document. | |
| 186 | */ | |
| 187 | public HTMLPreviewPane() { | |
| 188 | setStyle( "-fx-background-color: white;" ); | |
| 189 | ||
| 190 | // No need to append same prefix each time the HTML content is updated. | |
| 191 | mHtmlDocument.append( HTML_PREFIX ); | |
| 192 | mHtmlPrefixLength = mHtmlDocument.length(); | |
| 193 | ||
| 194 | // Inject an SVG renderer that produces high-quality SVG buffered images. | |
| 195 | final var factory = new ChainedReplacedElementFactory(); | |
| 196 | factory.addFactory( new SvgReplacedElementFactory() ); | |
| 197 | factory.addFactory( new SwingReplacedElementFactory( | |
| 198 | NO_OP_REPAINT_LISTENER, mImageLoader ) ); | |
| 199 | ||
| 200 | final var context = getSharedContext(); | |
| 201 | final var textRenderer = context.getTextRenderer(); | |
| 202 | context.setReplacedElementFactory( factory ); | |
| 203 | textRenderer.setSmoothingThreshold( 0 ); | |
| 204 | ||
| 205 | setContent( mScrollPane ); | |
| 206 | mHtmlRenderer.addDocumentListener( mDocHandler ); | |
| 207 | mHtmlRenderer.addComponentListener( new ResizeListener() ); | |
| 208 | ||
| 209 | // The default mouse click listener attempts navigation within the | |
| 210 | // preview panel. We want to usurp that behaviour to open the link in | |
| 211 | // a platform-specific browser. | |
| 212 | for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) { | |
| 213 | if( !(listener instanceof HoverListener) ) { | |
| 214 | mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener ); | |
| 215 | } | |
| 216 | } | |
| 217 | ||
| 218 | mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() ); | |
| 219 | } | |
| 220 | ||
| 221 | /** | |
| 222 | * Updates the internal HTML source, loads it into the preview pane, then | |
| 223 | * scrolls to the caret position. | |
| 224 | * | |
| 225 | * @param html The new HTML document to display. | |
| 226 | */ | |
| 227 | public void process( final String html ) { | |
| 228 | final Document jsoupDoc = Jsoup.parse( decorate( html ) ); | |
| 229 | final org.w3c.dom.Document w3cDoc = W3C_DOM.fromJsoup( jsoupDoc ); | |
| 230 | ||
| 231 | ||
| 232 | // Access to a Swing component must occur from the Event Dispatch | |
| 233 | // thread according to Swing threading restrictions. | |
| 234 | invokeLater( | |
| 235 | () -> mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), NS_HANDLER ) | |
| 236 | ); | |
| 237 | } | |
| 238 | ||
| 239 | public void clear() { | |
| 240 | process( "" ); | |
| 241 | } | |
| 242 | ||
| 243 | /** | |
| 244 | * Scrolls to an anchor link. The anchor links are injected when the | |
| 245 | * HTML document is created. | |
| 246 | * | |
| 247 | * @param id The unique anchor link identifier. | |
| 248 | */ | |
| 249 | public void tryScrollTo( final int id ) { | |
| 250 | final ChangeListener<Boolean> listener = new ChangeListener<>() { | |
| 251 | @Override | |
| 252 | public void changed( | |
| 253 | final ObservableValue<? extends Boolean> observable, | |
| 254 | final Boolean oldValue, | |
| 255 | final Boolean newValue ) { | |
| 256 | if( newValue ) { | |
| 257 | scrollTo( id ); | |
| 258 | ||
| 259 | mDocHandler.readyProperty().removeListener( this ); | |
| 260 | } | |
| 261 | } | |
| 262 | }; | |
| 263 | ||
| 264 | mDocHandler.readyProperty().addListener( listener ); | |
| 265 | } | |
| 266 | ||
| 267 | /** | |
| 268 | * Scrolls to the closest element matching the given identifier without | |
| 269 | * waiting for the document to be ready. Be sure the document is ready | |
| 270 | * before calling this method. | |
| 271 | * | |
| 272 | * @param id Paragraph index. | |
| 273 | */ | |
| 274 | public void scrollTo( final int id ) { | |
| 275 | if( id < 2 ) { | |
| 276 | scrollToTop(); | |
| 277 | } | |
| 278 | else { | |
| 279 | Box box = findPrevBox( id ); | |
| 280 | box = box == null ? findNextBox( id + 1 ) : box; | |
| 281 | ||
| 282 | if( box == null ) { | |
| 283 | scrollToBottom(); | |
| 284 | } | |
| 285 | else { | |
| 286 | scrollTo( box ); | |
| 287 | } | |
| 288 | } | |
| 289 | } | |
| 290 | ||
| 291 | private Box findPrevBox( final int id ) { | |
| 292 | int prevId = id; | |
| 293 | Box box = null; | |
| 294 | ||
| 295 | while( prevId > 0 && (box = getBoxById( PARAGRAPH_ID_PREFIX + prevId )) == null ) { | |
| 296 | prevId--; | |
| 297 | } | |
| 298 | ||
| 299 | return box; | |
| 300 | } | |
| 301 | ||
| 302 | private Box findNextBox( final int id ) { | |
| 303 | int nextId = id; | |
| 304 | Box box = null; | |
| 305 | ||
| 306 | while( nextId - id < 5 && | |
| 307 | (box = getBoxById( PARAGRAPH_ID_PREFIX + nextId )) == null ) { | |
| 308 | nextId++; | |
| 309 | } | |
| 310 | ||
| 311 | return box; | |
| 312 | } | |
| 313 | ||
| 314 | private void scrollTo( final Point point ) { | |
| 315 | invokeLater( () -> mHtmlRenderer.scrollTo( point ) ); | |
| 316 | } | |
| 317 | ||
| 318 | private void scrollTo( final Box box ) { | |
| 319 | scrollTo( createPoint( box ) ); | |
| 320 | } | |
| 321 | ||
| 322 | private void scrollToY( final int y ) { | |
| 323 | scrollTo( new Point( 0, y ) ); | |
| 324 | } | |
| 325 | ||
| 326 | private void scrollToTop() { | |
| 327 | scrollToY( 0 ); | |
| 328 | } | |
| 329 | ||
| 330 | private void scrollToBottom() { | |
| 331 | scrollToY( mHtmlRenderer.getHeight() ); | |
| 332 | } | |
| 333 | ||
| 334 | private Box getBoxById( final String id ) { | |
| 335 | return getSharedContext().getBoxById( id ); | |
| 336 | } | |
| 337 | ||
| 338 | private String decorate( final String html ) { | |
| 339 | // Trim the HTML back to only the prefix. | |
| 340 | mHtmlDocument.setLength( mHtmlPrefixLength ); | |
| 341 | ||
| 342 | // Write the HTML body element followed by closing tags. | |
| 343 | return mHtmlDocument.append( html ).append( HTML_SUFFIX ).toString(); | |
| 344 | } | |
| 345 | ||
| 346 | public Path getPath() { | |
| 347 | return mPath; | |
| 348 | } | |
| 349 | ||
| 350 | public void setPath( final Path path ) { | |
| 351 | assert path != null; | |
| 352 | mPath = path; | |
| 353 | } | |
| 354 | ||
| 355 | /** | |
| 356 | * Content to embed in a panel. | |
| 357 | * | |
| 358 | * @return The content to display to the user. | |
| 359 | */ | |
| 360 | public Node getNode() { | |
| 361 | return this; | |
| 362 | } | |
| 363 | ||
| 364 | public JScrollPane getScrollPane() { | |
| 365 | return mScrollPane; | |
| 366 | } | |
| 367 | ||
| 368 | public JScrollBar getVerticalScrollBar() { | |
| 369 | return getScrollPane().getVerticalScrollBar(); | |
| 370 | } | |
| 371 | ||
| 372 | /** | |
| 373 | * Creates a {@link Point} to use as a reference for scrolling to the area | |
| 374 | * described by the given {@link Box}. The {@link Box} coordinates are used | |
| 375 | * to populate the {@link Point}'s location, with minor adjustments for | |
| 376 | * vertical centering. | |
| 377 | * | |
| 378 | * @param box The {@link Box} that represents a scrolling anchor reference. | |
| 379 | * @return A coordinate suitable for scrolling to. | |
| 380 | */ | |
| 381 | private Point createPoint( final Box box ) { | |
| 382 | assert box != null; | |
| 383 | ||
| 384 | int x = box.getAbsX(); | |
| 385 | ||
| 386 | // Scroll back up by half the height of the scroll bar to keep the typing | |
| 387 | // area within the view port. Otherwise the view port will have jumped too | |
| 388 | // high up and the whatever gets typed won't be visible. | |
| 389 | int y = max( | |
| 390 | box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2), | |
| 391 | 0 ); | |
| 392 | ||
| 393 | if( !box.getStyle().isInline() ) { | |
| 394 | final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() ); | |
| 395 | x += margin.left(); | |
| 396 | y += margin.top(); | |
| 397 | } | |
| 398 | ||
| 399 | return new Point( x, y ); | |
| 400 | } | |
| 401 | ||
| 402 | private String getBaseUrl() { | |
| 403 | final Path basePath = getPath(); | |
| 404 | final Path parent = basePath == null ? null : basePath.getParent(); | |
| 405 | ||
| 406 | return parent == null ? "" : parent.toUri().toString(); | |
| 407 | } | |
| 408 | ||
| 409 | private SharedContext getSharedContext() { | |
| 410 | return mHtmlRenderer.getSharedContext(); | |
| 411 | } | |
| 412 | } | |
| 413 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preview; | |
| 29 | ||
| 30 | import com.scrivenvar.preferences.UserPreferences; | |
| 31 | import com.whitemagicsoftware.tex.*; | |
| 32 | import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D; | |
| 33 | import javafx.beans.property.IntegerProperty; | |
| 34 | import org.w3c.dom.Document; | |
| 35 | ||
| 36 | import java.util.function.Supplier; | |
| 37 | ||
| 38 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 39 | ||
| 40 | /** | |
| 41 | * Responsible for rendering formulas as scalable vector graphics (SVG). | |
| 42 | */ | |
| 43 | public class MathRenderer { | |
| 44 | ||
| 45 | /** | |
| 46 | * Default font size in points. | |
| 47 | */ | |
| 48 | private static final float FONT_SIZE = 20f; | |
| 49 | ||
| 50 | private final TeXFont mTeXFont = createDefaultTeXFont( FONT_SIZE ); | |
| 51 | private final TeXEnvironment mEnvironment = createTeXEnvironment( mTeXFont ); | |
| 52 | private final SvgDomGraphics2D mGraphics = createSvgDomGraphics2D(); | |
| 53 | ||
| 54 | public MathRenderer() { | |
| 55 | mGraphics.scale( FONT_SIZE, FONT_SIZE ); | |
| 56 | } | |
| 57 | ||
| 58 | /** | |
| 59 | * This method only takes a few seconds to generate | |
| 60 | * | |
| 61 | * @param equation A mathematical expression to render. | |
| 62 | * @return The given string with all formulas transformed into SVG format. | |
| 63 | */ | |
| 64 | public Document render( final String equation ) { | |
| 65 | final var formula = new TeXFormula( equation ); | |
| 66 | final var box = formula.createBox( mEnvironment ); | |
| 67 | final var l = new TeXLayout( box, FONT_SIZE ); | |
| 68 | ||
| 69 | mGraphics.initialize( l.getWidth(), l.getHeight() ); | |
| 70 | box.draw( mGraphics, l.getX(), l.getY() ); | |
| 71 | return mGraphics.toDom(); | |
| 72 | } | |
| 73 | ||
| 74 | @SuppressWarnings("SameParameterValue") | |
| 75 | private TeXFont createDefaultTeXFont( final float fontSize ) { | |
| 76 | return create( () -> new DefaultTeXFont( fontSize ) ); | |
| 77 | } | |
| 78 | ||
| 79 | private TeXEnvironment createTeXEnvironment( final TeXFont texFont ) { | |
| 80 | return create( () -> new TeXEnvironment( texFont ) ); | |
| 81 | } | |
| 82 | ||
| 83 | private SvgDomGraphics2D createSvgDomGraphics2D() { | |
| 84 | return create( SvgDomGraphics2D::new ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Tries to instantiate a given object, returning {@code null} on failure. | |
| 89 | * The failure message is bubbled up to to the user interface. | |
| 90 | * | |
| 91 | * @param supplier Creates an instance. | |
| 92 | * @param <T> The type of instance being created. | |
| 93 | * @return An instance of the parameterized type or {@code null} upon error. | |
| 94 | */ | |
| 95 | private <T> T create( final Supplier<T> supplier ) { | |
| 96 | try { | |
| 97 | return supplier.get(); | |
| 98 | } catch( final Exception ex ) { | |
| 99 | alert( ex ); | |
| 100 | return null; | |
| 101 | } | |
| 102 | } | |
| 103 | } | |
| 104 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preview; | |
| 29 | ||
| 30 | import java.util.HashMap; | |
| 31 | import java.util.Map; | |
| 32 | ||
| 33 | import static java.awt.RenderingHints.*; | |
| 34 | import static java.awt.Toolkit.getDefaultToolkit; | |
| 35 | ||
| 36 | /** | |
| 37 | * Responsible for supplying consistent rendering hints throughout the | |
| 38 | * application, such as image rendering for {@link SvgRasterizer}. | |
| 39 | */ | |
| 40 | @SuppressWarnings("rawtypes") | |
| 41 | public class RenderingSettings { | |
| 42 | ||
| 43 | /** | |
| 44 | * Default hints for high-quality rendering that may be changed by | |
| 45 | * the system's rendering hints. | |
| 46 | */ | |
| 47 | private static final Map<Object, Object> DEFAULT_HINTS = Map.of( | |
| 48 | KEY_ANTIALIASING, | |
| 49 | VALUE_ANTIALIAS_ON, | |
| 50 | KEY_ALPHA_INTERPOLATION, | |
| 51 | VALUE_ALPHA_INTERPOLATION_QUALITY, | |
| 52 | KEY_COLOR_RENDERING, | |
| 53 | VALUE_COLOR_RENDER_QUALITY, | |
| 54 | KEY_DITHERING, | |
| 55 | VALUE_DITHER_DISABLE, | |
| 56 | KEY_FRACTIONALMETRICS, | |
| 57 | VALUE_FRACTIONALMETRICS_ON, | |
| 58 | KEY_INTERPOLATION, | |
| 59 | VALUE_INTERPOLATION_BICUBIC, | |
| 60 | KEY_RENDERING, | |
| 61 | VALUE_RENDER_QUALITY, | |
| 62 | KEY_STROKE_CONTROL, | |
| 63 | VALUE_STROKE_PURE, | |
| 64 | KEY_TEXT_ANTIALIASING, | |
| 65 | VALUE_TEXT_ANTIALIAS_ON | |
| 66 | ); | |
| 67 | ||
| 68 | /** | |
| 69 | * Shared hints for high-quality rendering. | |
| 70 | */ | |
| 71 | public static final Map<Object, Object> RENDERING_HINTS = new HashMap<>( | |
| 72 | DEFAULT_HINTS | |
| 73 | ); | |
| 74 | ||
| 75 | static { | |
| 76 | final var toolkit = getDefaultToolkit(); | |
| 77 | final var hints = toolkit.getDesktopProperty( "awt.font.desktophints" ); | |
| 78 | ||
| 79 | if( hints instanceof Map ) { | |
| 80 | final var map = (Map) hints; | |
| 81 | for( final var key : map.keySet() ) { | |
| 82 | final var hint = map.get( key ); | |
| 83 | RENDERING_HINTS.put( key, hint ); | |
| 84 | } | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Prevent instantiation as per Joshua Bloch's recommendation. | |
| 90 | */ | |
| 91 | private RenderingSettings() { | |
| 92 | } | |
| 93 | } | |
| 94 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preview; | |
| 29 | ||
| 30 | import org.apache.batik.anim.dom.SAXSVGDocumentFactory; | |
| 31 | import org.apache.batik.gvt.renderer.ImageRenderer; | |
| 32 | import org.apache.batik.transcoder.TranscoderException; | |
| 33 | import org.apache.batik.transcoder.TranscoderInput; | |
| 34 | import org.apache.batik.transcoder.TranscoderOutput; | |
| 35 | import org.apache.batik.transcoder.image.ImageTranscoder; | |
| 36 | import org.w3c.dom.Document; | |
| 37 | import org.w3c.dom.Element; | |
| 38 | ||
| 39 | import javax.xml.transform.Transformer; | |
| 40 | import javax.xml.transform.TransformerConfigurationException; | |
| 41 | import javax.xml.transform.TransformerFactory; | |
| 42 | import javax.xml.transform.dom.DOMSource; | |
| 43 | import javax.xml.transform.stream.StreamResult; | |
| 44 | import java.awt.*; | |
| 45 | import java.awt.image.BufferedImage; | |
| 46 | import java.io.IOException; | |
| 47 | import java.io.StringReader; | |
| 48 | import java.io.StringWriter; | |
| 49 | import java.net.URL; | |
| 50 | import java.text.NumberFormat; | |
| 51 | ||
| 52 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 53 | import static com.scrivenvar.preview.RenderingSettings.RENDERING_HINTS; | |
| 54 | import static java.awt.image.BufferedImage.TYPE_INT_RGB; | |
| 55 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 56 | import static java.text.NumberFormat.getIntegerInstance; | |
| 57 | import static javax.xml.transform.OutputKeys.*; | |
| 58 | import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH; | |
| 59 | import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName; | |
| 60 | ||
| 61 | /** | |
| 62 | * Responsible for converting SVG images into rasterized PNG images. | |
| 63 | */ | |
| 64 | public class SvgRasterizer { | |
| 65 | private static final SAXSVGDocumentFactory FACTORY_DOM = | |
| 66 | new SAXSVGDocumentFactory( getXMLParserClassName() ); | |
| 67 | ||
| 68 | private static final TransformerFactory FACTORY_TRANSFORM = | |
| 69 | TransformerFactory.newInstance(); | |
| 70 | ||
| 71 | private static final Transformer sTransformer; | |
| 72 | ||
| 73 | static { | |
| 74 | Transformer t; | |
| 75 | ||
| 76 | try { | |
| 77 | t = FACTORY_TRANSFORM.newTransformer(); | |
| 78 | t.setOutputProperty( OMIT_XML_DECLARATION, "yes" ); | |
| 79 | t.setOutputProperty( METHOD, "xml" ); | |
| 80 | t.setOutputProperty( INDENT, "no" ); | |
| 81 | t.setOutputProperty( ENCODING, UTF_8.name() ); | |
| 82 | } catch( final TransformerConfigurationException e ) { | |
| 83 | t = null; | |
| 84 | } | |
| 85 | ||
| 86 | sTransformer = t; | |
| 87 | } | |
| 88 | ||
| 89 | private static final NumberFormat INT_FORMAT = getIntegerInstance(); | |
| 90 | ||
| 91 | public static final BufferedImage BROKEN_IMAGE_PLACEHOLDER; | |
| 92 | ||
| 93 | /** | |
| 94 | * A FontAwesome camera icon, cleft asunder. | |
| 95 | */ | |
| 96 | public static final String BROKEN_IMAGE_SVG = | |
| 97 | "<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www" + | |
| 98 | ".w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c" + | |
| 99 | ".332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path " + | |
| 100 | "d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531" + | |
| 101 | ".03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2" + | |
| 102 | ".511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-" + | |
| 103 | ".367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1" + | |
| 104 | ".699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 " + | |
| 105 | "2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094" + | |
| 106 | ".0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5" + | |
| 107 | ".0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-" + | |
| 108 | ".195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4" + | |
| 109 | ".191406-3.652344.207031-.015625.414063-.015625.617187-.007812l" + | |
| 110 | ".933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2" + | |
| 111 | ".820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3" + | |
| 112 | ".429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3" + | |
| 113 | ".429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 " + | |
| 114 | ".140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281" + | |
| 115 | ".808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2" + | |
| 116 | ".472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4" + | |
| 117 | ".285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1" + | |
| 118 | ".9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-" + | |
| 119 | ".667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1" + | |
| 120 | ".253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 " + | |
| 121 | "0'/></g></svg>"; | |
| 122 | ||
| 123 | static { | |
| 124 | // The width and height cannot be embedded in the SVG above because the | |
| 125 | // path element values are relative to the viewBox dimensions. | |
| 126 | final int w = 75; | |
| 127 | final int h = 75; | |
| 128 | BufferedImage image; | |
| 129 | ||
| 130 | try { | |
| 131 | image = rasterizeString( BROKEN_IMAGE_SVG, w ); | |
| 132 | } catch( final Exception e ) { | |
| 133 | image = new BufferedImage( w, h, TYPE_INT_RGB ); | |
| 134 | final var graphics = (Graphics2D) image.getGraphics(); | |
| 135 | graphics.setRenderingHints( RENDERING_HINTS ); | |
| 136 | ||
| 137 | // Fall back to a (\) symbol. | |
| 138 | graphics.setColor( new Color( 204, 204, 204 ) ); | |
| 139 | graphics.fillRect( 0, 0, w, h ); | |
| 140 | graphics.setColor( new Color( 255, 204, 204 ) ); | |
| 141 | graphics.setStroke( new BasicStroke( 4 ) ); | |
| 142 | graphics.drawOval( w / 4, h / 4, w / 2, h / 2 ); | |
| 143 | graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI), | |
| 144 | h / 4 + (int) (w / 4 / Math.PI), | |
| 145 | w / 2 + w / 4 - (int) (w / 4 / Math.PI), | |
| 146 | h / 2 + h / 4 - (int) (w / 4 / Math.PI) ); | |
| 147 | } | |
| 148 | ||
| 149 | BROKEN_IMAGE_PLACEHOLDER = image; | |
| 150 | } | |
| 151 | ||
| 152 | /** | |
| 153 | * Responsible for creating a new {@link ImageRenderer} implementation that | |
| 154 | * can render a DOM as an SVG image. | |
| 155 | */ | |
| 156 | private static class BufferedImageTranscoder extends ImageTranscoder { | |
| 157 | private BufferedImage mImage; | |
| 158 | ||
| 159 | @Override | |
| 160 | public BufferedImage createImage( final int w, final int h ) { | |
| 161 | return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB ); | |
| 162 | } | |
| 163 | ||
| 164 | @Override | |
| 165 | public void writeImage( | |
| 166 | final BufferedImage image, final TranscoderOutput output ) { | |
| 167 | mImage = image; | |
| 168 | } | |
| 169 | ||
| 170 | public BufferedImage getImage() { | |
| 171 | return mImage; | |
| 172 | } | |
| 173 | ||
| 174 | @Override | |
| 175 | protected ImageRenderer createRenderer() { | |
| 176 | final ImageRenderer renderer = super.createRenderer(); | |
| 177 | final RenderingHints hints = renderer.getRenderingHints(); | |
| 178 | hints.putAll( RENDERING_HINTS ); | |
| 179 | ||
| 180 | renderer.setRenderingHints( hints ); | |
| 181 | ||
| 182 | return renderer; | |
| 183 | } | |
| 184 | } | |
| 185 | ||
| 186 | /** | |
| 187 | * Rasterizes the vector graphic file at the given URL. If any exception | |
| 188 | * happens, a red circle is returned instead. | |
| 189 | * | |
| 190 | * @param url The URL to a vector graphic file, which must include the | |
| 191 | * protocol scheme (such as file:// or https://). | |
| 192 | * @param width The number of pixels wide to render the image. The aspect | |
| 193 | * ratio is maintained. | |
| 194 | * @return Either the rasterized image upon success or a red circle. | |
| 195 | */ | |
| 196 | public static BufferedImage rasterize( final String url, final int width ) { | |
| 197 | try { | |
| 198 | return rasterize( new URL( url ), width ); | |
| 199 | } catch( final Exception ex ) { | |
| 200 | alert( ex ); | |
| 201 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 202 | } | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * Rasterizes the given document into an image. | |
| 207 | * | |
| 208 | * @param svg The SVG {@link Document} to rasterize. | |
| 209 | * @param width The rasterized image's width (in pixels). | |
| 210 | * @return The rasterized image. | |
| 211 | * @throws TranscoderException Signifies an issue with the input document. | |
| 212 | */ | |
| 213 | public static BufferedImage rasterize( final Document svg, final int width ) | |
| 214 | throws TranscoderException { | |
| 215 | final var transcoder = new BufferedImageTranscoder(); | |
| 216 | final var input = new TranscoderInput( svg ); | |
| 217 | ||
| 218 | transcoder.addTranscodingHint( KEY_WIDTH, (float) width ); | |
| 219 | transcoder.transcode( input, null ); | |
| 220 | ||
| 221 | return transcoder.getImage(); | |
| 222 | } | |
| 223 | ||
| 224 | /** | |
| 225 | * Converts an SVG drawing into a rasterized image that can be drawn on | |
| 226 | * a graphics context. | |
| 227 | * | |
| 228 | * @param url The path to the image (can be web address). | |
| 229 | * @param width Scale the image width to this size (aspect ratio is | |
| 230 | * maintained). | |
| 231 | * @return The vector graphic transcoded into a raster image format. | |
| 232 | * @throws IOException Could not read the vector graphic. | |
| 233 | * @throws TranscoderException Could not convert the vector graphic to an | |
| 234 | * instance of {@link Image}. | |
| 235 | */ | |
| 236 | public static BufferedImage rasterize( final URL url, final int width ) | |
| 237 | throws IOException, TranscoderException { | |
| 238 | return rasterize( FACTORY_DOM.createDocument( url.toString() ), width ); | |
| 239 | } | |
| 240 | ||
| 241 | public static BufferedImage rasterize( final Document document ) { | |
| 242 | try { | |
| 243 | final var root = document.getDocumentElement(); | |
| 244 | final var width = root.getAttribute( "width" ); | |
| 245 | return rasterize( document, INT_FORMAT.parse( width ).intValue() ); | |
| 246 | } catch( final Exception ex ) { | |
| 247 | alert( ex ); | |
| 248 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 249 | } | |
| 250 | } | |
| 251 | ||
| 252 | /** | |
| 253 | * Converts an SVG string into a rasterized image that can be drawn on | |
| 254 | * a graphics context. | |
| 255 | * | |
| 256 | * @param svg The SVG xml document. | |
| 257 | * @param w Scale the image width to this size (aspect ratio is | |
| 258 | * maintained). | |
| 259 | * @return The vector graphic transcoded into a raster image format. | |
| 260 | * @throws TranscoderException Could not convert the vector graphic to an | |
| 261 | * instance of {@link Image}. | |
| 262 | */ | |
| 263 | public static BufferedImage rasterizeString( final String svg, final int w ) | |
| 264 | throws IOException, TranscoderException { | |
| 265 | return rasterize( toDocument( svg ), w ); | |
| 266 | } | |
| 267 | ||
| 268 | /** | |
| 269 | * Converts an SVG string into a rasterized image that can be drawn on | |
| 270 | * a graphics context. The dimensions are determined from the document. | |
| 271 | * | |
| 272 | * @param xml The SVG xml document. | |
| 273 | * @return The vector graphic transcoded into a raster image format. | |
| 274 | */ | |
| 275 | public static BufferedImage rasterizeString( final String xml ) { | |
| 276 | try { | |
| 277 | final var document = toDocument( xml ); | |
| 278 | final var root = document.getDocumentElement(); | |
| 279 | final var width = root.getAttribute( "width" ); | |
| 280 | return rasterizeString( xml, INT_FORMAT.parse( width ).intValue() ); | |
| 281 | } catch( final Exception ex ) { | |
| 282 | alert( ex ); | |
| 283 | return BROKEN_IMAGE_PLACEHOLDER; | |
| 284 | } | |
| 285 | } | |
| 286 | ||
| 287 | /** | |
| 288 | * Converts an SVG XML string into a new {@link Document} instance. | |
| 289 | * | |
| 290 | * @param xml The XML containing SVG elements. | |
| 291 | * @return The SVG contents parsed into a {@link Document} object model. | |
| 292 | * @throws IOException Could | |
| 293 | */ | |
| 294 | private static Document toDocument( final String xml ) throws IOException { | |
| 295 | try( final var reader = new StringReader( xml ) ) { | |
| 296 | return FACTORY_DOM.createSVGDocument( | |
| 297 | "http://www.w3.org/2000/svg", reader ); | |
| 298 | } | |
| 299 | } | |
| 300 | ||
| 301 | /** | |
| 302 | * Given a document object model (DOM) {@link Element}, this will convert that | |
| 303 | * element to a string. | |
| 304 | * | |
| 305 | * @param e The DOM node to convert to a string. | |
| 306 | * @return The DOM node as an escaped, plain text string. | |
| 307 | */ | |
| 308 | public static String toSvg( final Element e ) { | |
| 309 | try( final var writer = new StringWriter() ) { | |
| 310 | sTransformer.transform( new DOMSource( e ), new StreamResult( writer ) ); | |
| 311 | return writer.toString().replaceAll( "xmlns=\"\" ", "" ); | |
| 312 | } catch( final Exception ex ) { | |
| 313 | alert( ex ); | |
| 314 | } | |
| 315 | ||
| 316 | return BROKEN_IMAGE_SVG; | |
| 317 | } | |
| 318 | } | |
| 319 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.preview; | |
| 29 | ||
| 30 | import com.scrivenvar.util.BoundedCache; | |
| 31 | import org.apache.commons.io.FilenameUtils; | |
| 32 | import org.w3c.dom.Element; | |
| 33 | import org.xhtmlrenderer.extend.ReplacedElement; | |
| 34 | import org.xhtmlrenderer.extend.ReplacedElementFactory; | |
| 35 | import org.xhtmlrenderer.extend.UserAgentCallback; | |
| 36 | import org.xhtmlrenderer.layout.LayoutContext; | |
| 37 | import org.xhtmlrenderer.render.BlockBox; | |
| 38 | import org.xhtmlrenderer.simple.extend.FormSubmissionListener; | |
| 39 | import org.xhtmlrenderer.swing.ImageReplacedElement; | |
| 40 | ||
| 41 | import java.awt.image.BufferedImage; | |
| 42 | import java.util.Map; | |
| 43 | import java.util.function.Function; | |
| 44 | ||
| 45 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 46 | import static com.scrivenvar.preview.SvgRasterizer.rasterize; | |
| 47 | import static com.scrivenvar.processors.markdown.tex.TeXNode.HTML_TEX; | |
| 48 | ||
| 49 | /** | |
| 50 | * Responsible for running {@link SvgRasterizer} on SVG images detected within | |
| 51 | * a document to transform them into rasterized versions. | |
| 52 | */ | |
| 53 | public class SvgReplacedElementFactory implements ReplacedElementFactory { | |
| 54 | ||
| 55 | /** | |
| 56 | * Prevent instantiation until needed. | |
| 57 | */ | |
| 58 | private static class MathRendererContainer { | |
| 59 | private static final MathRenderer INSTANCE = new MathRenderer(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Returns the singleton instance for rendering math symbols. | |
| 64 | * | |
| 65 | * @return A non-null instance, loaded, configured, and ready to render math. | |
| 66 | */ | |
| 67 | public static MathRenderer getInstance() { | |
| 68 | return MathRendererContainer.INSTANCE; | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * SVG filename extension maps to an SVG image element. | |
| 73 | */ | |
| 74 | private static final String SVG_FILE = "svg"; | |
| 75 | ||
| 76 | private static final String HTML_IMAGE = "img"; | |
| 77 | private static final String HTML_IMAGE_SRC = "src"; | |
| 78 | ||
| 79 | /** | |
| 80 | * A bounded cache that removes the oldest image if the maximum number of | |
| 81 | * cached images has been reached. This constrains the number of images | |
| 82 | * loaded into memory. | |
| 83 | */ | |
| 84 | private final Map<String, BufferedImage> mImageCache = | |
| 85 | new BoundedCache<>( 150 ); | |
| 86 | ||
| 87 | @Override | |
| 88 | public ReplacedElement createReplacedElement( | |
| 89 | final LayoutContext c, | |
| 90 | final BlockBox box, | |
| 91 | final UserAgentCallback uac, | |
| 92 | final int cssWidth, | |
| 93 | final int cssHeight ) { | |
| 94 | BufferedImage image = null; | |
| 95 | final var e = box.getElement(); | |
| 96 | ||
| 97 | if( e != null ) { | |
| 98 | try { | |
| 99 | final var nodeName = e.getNodeName(); | |
| 100 | ||
| 101 | if( HTML_IMAGE.equals( nodeName ) ) { | |
| 102 | final var src = e.getAttribute( HTML_IMAGE_SRC ); | |
| 103 | final var ext = FilenameUtils.getExtension( src ); | |
| 104 | ||
| 105 | if( SVG_FILE.equalsIgnoreCase( ext ) ) { | |
| 106 | image = getCachedImage( | |
| 107 | src, svg -> rasterize( svg, box.getContentWidth() ) ); | |
| 108 | } | |
| 109 | } | |
| 110 | else if( HTML_TEX.equals( nodeName ) ) { | |
| 111 | // Convert the TeX element to a raster graphic if not yet cached. | |
| 112 | final var src = e.getTextContent(); | |
| 113 | image = getCachedImage( | |
| 114 | src, __ -> rasterize( getInstance().render( src ) ) | |
| 115 | ); | |
| 116 | } | |
| 117 | } catch( final Exception ex ) { | |
| 118 | alert( ex ); | |
| 119 | } | |
| 120 | } | |
| 121 | ||
| 122 | if( image != null ) { | |
| 123 | final var w = image.getWidth( null ); | |
| 124 | final var h = image.getHeight( null ); | |
| 125 | ||
| 126 | return new ImageReplacedElement( image, w, h ); | |
| 127 | } | |
| 128 | ||
| 129 | return null; | |
| 130 | } | |
| 131 | ||
| 132 | @Override | |
| 133 | public void reset() { | |
| 134 | } | |
| 135 | ||
| 136 | @Override | |
| 137 | public void remove( final Element e ) { | |
| 138 | } | |
| 139 | ||
| 140 | @Override | |
| 141 | public void setFormSubmissionListener( FormSubmissionListener listener ) { | |
| 142 | } | |
| 143 | ||
| 144 | /** | |
| 145 | * Returns an image associated with a string; the string's pre-computed | |
| 146 | * hash code is returned as the string value, making this operation very | |
| 147 | * quick to return the corresponding {@link BufferedImage}. | |
| 148 | * | |
| 149 | * @param src The source used for the key into the image cache. | |
| 150 | * @param rasterizer {@link Function} to call to rasterize an image. | |
| 151 | * @return The image that corresponds to the given source string. | |
| 152 | */ | |
| 153 | private BufferedImage getCachedImage( | |
| 154 | final String src, final Function<String, BufferedImage> rasterizer ) { | |
| 155 | return mImageCache.computeIfAbsent( src, __ -> rasterizer.apply( src ) ); | |
| 156 | } | |
| 157 | } | |
| 158 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | /** | |
| 31 | * Responsible for transforming a document through a variety of chained | |
| 32 | * handlers. If there are conditions where this handler should not process the | |
| 33 | * entire chain, create a second handler, or split the chain into reusable | |
| 34 | * sub-chains. | |
| 35 | * | |
| 36 | * @param <T> The type of object to process. | |
| 37 | */ | |
| 38 | public abstract class AbstractProcessor<T> implements Processor<T> { | |
| 39 | ||
| 40 | /** | |
| 41 | * Used while processing the entire chain; null to signify no more links. | |
| 42 | */ | |
| 43 | private final Processor<T> mNext; | |
| 44 | ||
| 45 | /** | |
| 46 | * Constructs a new default handler with no successor. | |
| 47 | */ | |
| 48 | protected AbstractProcessor() { | |
| 49 | this( null ); | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Constructs a new default handler with a given successor. | |
| 54 | * | |
| 55 | * @param successor The next processor in the chain. | |
| 56 | */ | |
| 57 | public AbstractProcessor( final Processor<T> successor ) { | |
| 58 | mNext = successor; | |
| 59 | } | |
| 60 | ||
| 61 | @Override | |
| 62 | public Processor<T> next() { | |
| 63 | return mNext; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * This algorithm is incorrect, but works for the one use case of removing | |
| 68 | * the ending HTML Preview Processor from the end of the processor chain. | |
| 69 | * The processor chain is immutable so this creates a succession of | |
| 70 | * delegators that wrap each processor in the chain, except for the one | |
| 71 | * to be removed. | |
| 72 | * <p> | |
| 73 | * An alternative is to update the {@link ProcessorFactory} with the ability | |
| 74 | * to create a processor chain devoid of an {@link HtmlPreviewProcessor}. | |
| 75 | * </p> | |
| 76 | * | |
| 77 | * @param removal The {@link Processor} to remove from the chain. | |
| 78 | * @return A delegating processor chain starting from this processor | |
| 79 | * onwards with the given processor removed from the chain. | |
| 80 | */ | |
| 81 | @Override | |
| 82 | public Processor<T> remove( final Class<? extends Processor<T>> removal ) { | |
| 83 | Processor<T> p = this; | |
| 84 | final ProcessorDelegator<T> head = new ProcessorDelegator<>( p ); | |
| 85 | ProcessorDelegator<T> result = head; | |
| 86 | ||
| 87 | while( p != null ) { | |
| 88 | final Processor<T> next = p.next(); | |
| 89 | ||
| 90 | if( next != null && next.getClass() != removal ) { | |
| 91 | final var delegator = new ProcessorDelegator<>( next ); | |
| 92 | ||
| 93 | result.setNext( delegator ); | |
| 94 | result = delegator; | |
| 95 | } | |
| 96 | ||
| 97 | p = p.next(); | |
| 98 | } | |
| 99 | ||
| 100 | return head; | |
| 101 | } | |
| 102 | ||
| 103 | private static final class ProcessorDelegator<T> | |
| 104 | extends AbstractProcessor<T> { | |
| 105 | private final Processor<T> mDelegate; | |
| 106 | private Processor<T> mNext; | |
| 107 | ||
| 108 | public ProcessorDelegator( final Processor<T> delegate ) { | |
| 109 | super( delegate ); | |
| 110 | ||
| 111 | assert delegate != null; | |
| 112 | ||
| 113 | mDelegate = delegate; | |
| 114 | } | |
| 115 | ||
| 116 | @Override | |
| 117 | public T apply( T t ) { | |
| 118 | return mDelegate.apply( t ); | |
| 119 | } | |
| 120 | ||
| 121 | protected void setNext( final Processor<T> next ) { | |
| 122 | mNext = next; | |
| 123 | } | |
| 124 | ||
| 125 | @Override | |
| 126 | public Processor<T> next() { | |
| 127 | return mNext; | |
| 128 | } | |
| 129 | } | |
| 130 | } | |
| 131 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | import static com.scrivenvar.processors.text.TextReplacementFactory.replace; | |
| 33 | ||
| 34 | /** | |
| 35 | * Processes interpolated string definitions in the document and inserts | |
| 36 | * their values into the post-processed text. The default variable syntax is | |
| 37 | * {@code $variable$}. | |
| 38 | */ | |
| 39 | public class DefinitionProcessor extends AbstractProcessor<String> { | |
| 40 | ||
| 41 | private final Map<String, String> mDefinitions; | |
| 42 | ||
| 43 | public DefinitionProcessor( | |
| 44 | final Processor<String> successor, final Map<String, String> map ) { | |
| 45 | super( successor ); | |
| 46 | mDefinitions = map; | |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * Processes the given text document by replacing variables with their values. | |
| 51 | * | |
| 52 | * @param text The document text that includes variables that should be | |
| 53 | * replaced with values when rendered as HTML. | |
| 54 | * @return The text with all variables replaced. | |
| 55 | */ | |
| 56 | @Override | |
| 57 | public String apply( final String text ) { | |
| 58 | return replace( text, getDefinitions() ); | |
| 59 | } | |
| 60 | ||
| 61 | /** | |
| 62 | * Returns the map to use for variable substitution. | |
| 63 | * | |
| 64 | * @return A map of variable names to values. | |
| 65 | */ | |
| 66 | protected Map<String, String> getDefinitions() { | |
| 67 | return mDefinitions; | |
| 68 | } | |
| 69 | } | |
| 70 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | import com.scrivenvar.preview.HTMLPreviewPane; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for notifying the HTMLPreviewPane when the succession chain has | |
| 34 | * updated. This decouples knowledge of changes to the editor panel from the | |
| 35 | * HTML preview panel as well as any processing that takes place before the | |
| 36 | * final HTML preview is rendered. This should be the last link in the processor | |
| 37 | * chain. | |
| 38 | */ | |
| 39 | public class HtmlPreviewProcessor extends AbstractProcessor<String> { | |
| 40 | ||
| 41 | // There is only one preview panel. | |
| 42 | private static HTMLPreviewPane sHtmlPreviewPane; | |
| 43 | ||
| 44 | /** | |
| 45 | * Constructs the end of a processing chain. | |
| 46 | * | |
| 47 | * @param htmlPreviewPane The pane to update with the post-processed document. | |
| 48 | */ | |
| 49 | public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) { | |
| 50 | sHtmlPreviewPane = htmlPreviewPane; | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * Update the preview panel using HTML from the succession chain. | |
| 55 | * | |
| 56 | * @param html The document content to render in the preview pane. The HTML | |
| 57 | * should not contain a doctype, head, or body tag, only | |
| 58 | * content to render within the body. | |
| 59 | * @return {@code null} to indicate no more processors in the chain. | |
| 60 | */ | |
| 61 | @Override | |
| 62 | public String apply( final String html ) { | |
| 63 | getHtmlPreviewPane().process( html ); | |
| 64 | ||
| 65 | // No more processing required. | |
| 66 | return null; | |
| 67 | } | |
| 68 | ||
| 69 | private HTMLPreviewPane getHtmlPreviewPane() { | |
| 70 | return sHtmlPreviewPane; | |
| 71 | } | |
| 72 | } | |
| 73 | 1 |
| 1 | /* | |
| 2 | * Copyright 2017 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | /** | |
| 31 | * This is the default processor used when an unknown filename extension is | |
| 32 | * encountered. | |
| 33 | */ | |
| 34 | public class IdentityProcessor extends AbstractProcessor<String> { | |
| 35 | ||
| 36 | /** | |
| 37 | * Passes the link to the super constructor. | |
| 38 | * | |
| 39 | * @param successor The next processor in the chain to use for text | |
| 40 | * processing. | |
| 41 | */ | |
| 42 | public IdentityProcessor( final Processor<String> successor ) { | |
| 43 | super( successor ); | |
| 44 | } | |
| 45 | ||
| 46 | /** | |
| 47 | * Returns the given string, modified with "pre" tags. | |
| 48 | * | |
| 49 | * @param t The string to return, enclosed in "pre" tags. | |
| 50 | * @return The value of t wrapped in "pre" tags. | |
| 51 | */ | |
| 52 | @Override | |
| 53 | public String apply( final String t ) { | |
| 54 | return "<pre>" + t + "</pre>"; | |
| 55 | } | |
| 56 | } | |
| 57 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | import com.scrivenvar.preferences.UserPreferences; | |
| 31 | import javafx.beans.property.ObjectProperty; | |
| 32 | import javafx.beans.property.StringProperty; | |
| 33 | ||
| 34 | import javax.script.ScriptEngine; | |
| 35 | import javax.script.ScriptEngineManager; | |
| 36 | import java.io.File; | |
| 37 | import java.nio.file.Path; | |
| 38 | import java.util.LinkedHashMap; | |
| 39 | import java.util.Map; | |
| 40 | import java.util.concurrent.atomic.AtomicBoolean; | |
| 41 | ||
| 42 | import static com.scrivenvar.Constants.STATUS_PARSE_ERROR; | |
| 43 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 44 | import static com.scrivenvar.processors.text.TextReplacementFactory.replace; | |
| 45 | import static com.scrivenvar.sigils.RSigilOperator.PREFIX; | |
| 46 | import static com.scrivenvar.sigils.RSigilOperator.SUFFIX; | |
| 47 | import static java.lang.Math.min; | |
| 48 | ||
| 49 | /** | |
| 50 | * Transforms a document containing R statements into Markdown. | |
| 51 | */ | |
| 52 | public final class InlineRProcessor extends DefinitionProcessor { | |
| 53 | /** | |
| 54 | * Constrain memory when typing new R expressions into the document. | |
| 55 | */ | |
| 56 | private static final int MAX_CACHED_R_STATEMENTS = 512; | |
| 57 | ||
| 58 | /** | |
| 59 | * Where to put document inline evaluated R expressions. | |
| 60 | */ | |
| 61 | private final Map<String, Object> mEvalCache = new LinkedHashMap<>() { | |
| 62 | @Override | |
| 63 | protected boolean removeEldestEntry( | |
| 64 | final Map.Entry<String, Object> eldest ) { | |
| 65 | return size() > MAX_CACHED_R_STATEMENTS; | |
| 66 | } | |
| 67 | }; | |
| 68 | ||
| 69 | /** | |
| 70 | * Only one editor is open at a time. | |
| 71 | */ | |
| 72 | private static final ScriptEngine ENGINE = | |
| 73 | (new ScriptEngineManager()).getEngineByName( "Renjin" ); | |
| 74 | ||
| 75 | private static final int PREFIX_LENGTH = PREFIX.length(); | |
| 76 | ||
| 77 | private final AtomicBoolean mDirty = new AtomicBoolean( false ); | |
| 78 | ||
| 79 | /** | |
| 80 | * Constructs a processor capable of evaluating R statements. | |
| 81 | * | |
| 82 | * @param successor Subsequent link in the processing chain. | |
| 83 | * @param map Resolved definitions map. | |
| 84 | */ | |
| 85 | public InlineRProcessor( | |
| 86 | final Processor<String> successor, | |
| 87 | final Map<String, String> map ) { | |
| 88 | super( successor, map ); | |
| 89 | ||
| 90 | bootstrapScriptProperty().addListener( | |
| 91 | ( ob, oldScript, newScript ) -> setDirty( true ) ); | |
| 92 | workingDirectoryProperty().addListener( | |
| 93 | ( ob, oldScript, newScript ) -> setDirty( true ) ); | |
| 94 | ||
| 95 | getUserPreferences().addSaveEventHandler( ( handler ) -> { | |
| 96 | if( isDirty() ) { | |
| 97 | init(); | |
| 98 | setDirty( false ); | |
| 99 | } | |
| 100 | } ); | |
| 101 | ||
| 102 | init(); | |
| 103 | } | |
| 104 | ||
| 105 | /** | |
| 106 | * Initialises the R code so that R can find imported libraries. Note that | |
| 107 | * any existing R functionality will not be overwritten if this method is | |
| 108 | * called multiple times. | |
| 109 | */ | |
| 110 | private void init() { | |
| 111 | final var bootstrap = getBootstrapScript(); | |
| 112 | ||
| 113 | if( !bootstrap.isBlank() ) { | |
| 114 | final var wd = getWorkingDirectory(); | |
| 115 | final var dir = wd.toString().replace( '\\', '/' ); | |
| 116 | final var map = getDefinitions(); | |
| 117 | map.put( "$application.r.working.directory$", dir ); | |
| 118 | ||
| 119 | eval( replace( bootstrap, map ) ); | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | /** | |
| 124 | * Sets the dirty flag to indicate that the bootstrap script or working | |
| 125 | * directory has been modified. Upon saving the preferences, if this flag | |
| 126 | * is true, then {@link #init()} will be called to reload the R environment. | |
| 127 | * | |
| 128 | * @param dirty Set to true to reload changes upon closing preferences. | |
| 129 | */ | |
| 130 | private void setDirty( final boolean dirty ) { | |
| 131 | mDirty.set( dirty ); | |
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * Answers whether R-related settings have been modified. | |
| 136 | * | |
| 137 | * @return {@code true} when the settings have changed. | |
| 138 | */ | |
| 139 | private boolean isDirty() { | |
| 140 | return mDirty.get(); | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Evaluates all R statements in the source document and inserts the | |
| 145 | * calculated value into the generated document. | |
| 146 | * | |
| 147 | * @param text The document text that includes variables that should be | |
| 148 | * replaced with values when rendered as HTML. | |
| 149 | * @return The generated document with output from all R statements | |
| 150 | * substituted with value returned from their execution. | |
| 151 | */ | |
| 152 | @Override | |
| 153 | public String apply( final String text ) { | |
| 154 | final int length = text.length(); | |
| 155 | ||
| 156 | // The * 2 is a wild guess at the ratio of R statements to the length | |
| 157 | // of text produced by those statements. | |
| 158 | final StringBuilder sb = new StringBuilder( length * 2 ); | |
| 159 | ||
| 160 | int prevIndex = 0; | |
| 161 | int currIndex = text.indexOf( PREFIX ); | |
| 162 | ||
| 163 | while( currIndex >= 0 ) { | |
| 164 | // Copy everything up to, but not including, an R statement (`r#). | |
| 165 | sb.append( text, prevIndex, currIndex ); | |
| 166 | ||
| 167 | // Jump to the start of the R statement. | |
| 168 | prevIndex = currIndex + PREFIX_LENGTH; | |
| 169 | ||
| 170 | // Find the statement ending (`), without indexing past the text boundary. | |
| 171 | currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) ); | |
| 172 | ||
| 173 | // Only evaluate inline R statements that have end delimiters. | |
| 174 | if( currIndex > 1 ) { | |
| 175 | // Extract the inline R statement to be evaluated. | |
| 176 | final String r = text.substring( prevIndex, currIndex ); | |
| 177 | ||
| 178 | // Pass the R statement into the R engine for evaluation. | |
| 179 | try { | |
| 180 | final Object result = evalText( r ); | |
| 181 | ||
| 182 | // Append the string representation of the result into the text. | |
| 183 | sb.append( result ); | |
| 184 | } catch( final Exception e ) { | |
| 185 | // If the string couldn't be parsed using R, append the statement | |
| 186 | // that failed to parse, instead of its evaluated value. | |
| 187 | sb.append( PREFIX ).append( r ).append( SUFFIX ); | |
| 188 | ||
| 189 | // Tell the user that there was a problem. | |
| 190 | alert( STATUS_PARSE_ERROR, e.getMessage(), currIndex ); | |
| 191 | } | |
| 192 | ||
| 193 | // Retain the R statement's ending position in the text. | |
| 194 | prevIndex = currIndex + 1; | |
| 195 | } | |
| 196 | ||
| 197 | // Find the start of the next inline R statement. | |
| 198 | currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) ); | |
| 199 | } | |
| 200 | ||
| 201 | // Copy from the previous index to the end of the string. | |
| 202 | return sb.append( text.substring( min( prevIndex, length ) ) ).toString(); | |
| 203 | } | |
| 204 | ||
| 205 | /** | |
| 206 | * Look up an R expression from the cache then return the resulting object. | |
| 207 | * If the R expression hasn't been cached, it'll first be evaluated. | |
| 208 | * | |
| 209 | * @param r The expression to evaluate. | |
| 210 | * @return The object resulting from the evaluation. | |
| 211 | */ | |
| 212 | private Object evalText( final String r ) { | |
| 213 | return mEvalCache.computeIfAbsent( r, v -> eval( r ) ); | |
| 214 | } | |
| 215 | ||
| 216 | /** | |
| 217 | * Evaluate an R expression and return the resulting object. | |
| 218 | * | |
| 219 | * @param r The expression to evaluate. | |
| 220 | * @return The object resulting from the evaluation. | |
| 221 | */ | |
| 222 | private Object eval( final String r ) { | |
| 223 | try { | |
| 224 | return getScriptEngine().eval( r ); | |
| 225 | } catch( final Exception ex ) { | |
| 226 | final String expr = r.substring( 0, min( r.length(), 30 ) ); | |
| 227 | alert( "Main.status.error.r", expr, ex.getMessage() ); | |
| 228 | } | |
| 229 | ||
| 230 | return ""; | |
| 231 | } | |
| 232 | ||
| 233 | /** | |
| 234 | * Return the given path if not {@code null}, otherwise return the path to | |
| 235 | * the user's directory. | |
| 236 | * | |
| 237 | * @return A non-null path. | |
| 238 | */ | |
| 239 | private Path getWorkingDirectory() { | |
| 240 | return getUserPreferences().getRDirectory().toPath(); | |
| 241 | } | |
| 242 | ||
| 243 | private ObjectProperty<File> workingDirectoryProperty() { | |
| 244 | return getUserPreferences().rDirectoryProperty(); | |
| 245 | } | |
| 246 | ||
| 247 | /** | |
| 248 | * Loads the R init script from the application's persisted preferences. | |
| 249 | * | |
| 250 | * @return A non-null string, possibly empty. | |
| 251 | */ | |
| 252 | private String getBootstrapScript() { | |
| 253 | return getUserPreferences().getRScript(); | |
| 254 | } | |
| 255 | ||
| 256 | private StringProperty bootstrapScriptProperty() { | |
| 257 | return getUserPreferences().rScriptProperty(); | |
| 258 | } | |
| 259 | ||
| 260 | private UserPreferences getUserPreferences() { | |
| 261 | return UserPreferences.getInstance(); | |
| 262 | } | |
| 263 | ||
| 264 | private ScriptEngine getScriptEngine() { | |
| 265 | return ENGINE; | |
| 266 | } | |
| 267 | } | |
| 268 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | import java.util.function.UnaryOperator; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for processing documents from one known format to another. | |
| 34 | * Processes the given content providing a transformation from one document | |
| 35 | * format into another. For example, this could convert from XML to text using | |
| 36 | * an XSLT processor, or from markdown to HTML. | |
| 37 | * | |
| 38 | * @param <T> The type of processor to create. | |
| 39 | */ | |
| 40 | public interface Processor<T> extends UnaryOperator<T> { | |
| 41 | ||
| 42 | /** | |
| 43 | * Removes the given processor from the chain, returning a new immutable | |
| 44 | * chain equivalent to this chain, but without the given processor. | |
| 45 | * | |
| 46 | * @param processor The {@link Processor} to remove from the chain. | |
| 47 | * @return A delegating processor chain starting from this processor | |
| 48 | * onwards with the given processor removed from the chain. | |
| 49 | */ | |
| 50 | Processor<T> remove( Class<? extends Processor<T>> processor ); | |
| 51 | ||
| 52 | /** | |
| 53 | * Adds a document processor to call after this processor finishes processing | |
| 54 | * the document given to the process method. | |
| 55 | * | |
| 56 | * @return The processor that should transform the document after this | |
| 57 | * instance has finished processing, or {@code null} if this is the last | |
| 58 | * processor in the chain. | |
| 59 | */ | |
| 60 | default Processor<T> next() { | |
| 61 | return null; | |
| 62 | } | |
| 63 | } | |
| 64 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | import com.scrivenvar.AbstractFileFactory; | |
| 31 | import com.scrivenvar.FileEditorTab; | |
| 32 | import com.scrivenvar.preview.HTMLPreviewPane; | |
| 33 | import com.scrivenvar.processors.markdown.MarkdownProcessor; | |
| 34 | ||
| 35 | import java.util.Map; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for creating processors capable of parsing, transforming, | |
| 39 | * interpolating, and rendering known file types. | |
| 40 | */ | |
| 41 | public class ProcessorFactory extends AbstractFileFactory { | |
| 42 | ||
| 43 | private final HTMLPreviewPane mPreviewPane; | |
| 44 | private final Map<String, String> mResolvedMap; | |
| 45 | private final Processor<String> mMarkdownProcessor; | |
| 46 | ||
| 47 | /** | |
| 48 | * Constructs a factory with the ability to create processors that can perform | |
| 49 | * text and caret processing to generate a final preview. | |
| 50 | * | |
| 51 | * @param previewPane Where the final output is rendered. | |
| 52 | * @param resolvedMap Flat map of definitions to replace before final render. | |
| 53 | */ | |
| 54 | public ProcessorFactory( | |
| 55 | final HTMLPreviewPane previewPane, | |
| 56 | final Map<String, String> resolvedMap ) { | |
| 57 | mPreviewPane = previewPane; | |
| 58 | mResolvedMap = resolvedMap; | |
| 59 | mMarkdownProcessor = createMarkdownProcessor(); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Creates a processor chain suitable for parsing and rendering the file | |
| 64 | * opened at the given tab. | |
| 65 | * | |
| 66 | * @param tab The tab containing a text editor, path, and caret position. | |
| 67 | * @return A processor that can render the given tab's text. | |
| 68 | */ | |
| 69 | public Processor<String> createProcessors( final FileEditorTab tab ) { | |
| 70 | return switch( lookup( tab.getPath() ) ) { | |
| 71 | case RMARKDOWN -> createRProcessor(); | |
| 72 | case SOURCE -> createMarkdownDefinitionProcessor(); | |
| 73 | case XML -> createXMLProcessor( tab ); | |
| 74 | case RXML -> createRXMLProcessor( tab ); | |
| 75 | default -> createIdentityProcessor(); | |
| 76 | }; | |
| 77 | } | |
| 78 | ||
| 79 | private Processor<String> createHTMLPreviewProcessor() { | |
| 80 | return new HtmlPreviewProcessor( getPreviewPane() ); | |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Creates and links the processors at the end of the processing chain. | |
| 85 | * | |
| 86 | * @return A markdown, caret replacement, and preview pane processor chain. | |
| 87 | */ | |
| 88 | private Processor<String> createMarkdownProcessor() { | |
| 89 | final var hpp = createHTMLPreviewProcessor(); | |
| 90 | return new MarkdownProcessor( hpp, getPreviewPane().getPath() ); | |
| 91 | } | |
| 92 | ||
| 93 | protected Processor<String> createIdentityProcessor() { | |
| 94 | final var hpp = createHTMLPreviewProcessor(); | |
| 95 | return new IdentityProcessor( hpp ); | |
| 96 | } | |
| 97 | ||
| 98 | protected Processor<String> createDefinitionProcessor( | |
| 99 | final Processor<String> p ) { | |
| 100 | return new DefinitionProcessor( p, getResolvedMap() ); | |
| 101 | } | |
| 102 | ||
| 103 | protected Processor<String> createMarkdownDefinitionProcessor() { | |
| 104 | final var tpc = getCommonProcessor(); | |
| 105 | return createDefinitionProcessor( tpc ); | |
| 106 | } | |
| 107 | ||
| 108 | protected Processor<String> createXMLProcessor( final FileEditorTab tab ) { | |
| 109 | final var tpc = getCommonProcessor(); | |
| 110 | final var xmlp = new XmlProcessor( tpc, tab.getPath() ); | |
| 111 | return createDefinitionProcessor( xmlp ); | |
| 112 | } | |
| 113 | ||
| 114 | protected Processor<String> createRProcessor() { | |
| 115 | final var tpc = getCommonProcessor(); | |
| 116 | final var rp = new InlineRProcessor( tpc, getResolvedMap() ); | |
| 117 | return new RVariableProcessor( rp, getResolvedMap() ); | |
| 118 | } | |
| 119 | ||
| 120 | protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) { | |
| 121 | final var tpc = getCommonProcessor(); | |
| 122 | final var xmlp = new XmlProcessor( tpc, tab.getPath() ); | |
| 123 | final var rp = new InlineRProcessor( xmlp, getResolvedMap() ); | |
| 124 | return new RVariableProcessor( rp, getResolvedMap() ); | |
| 125 | } | |
| 126 | ||
| 127 | private HTMLPreviewPane getPreviewPane() { | |
| 128 | return mPreviewPane; | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * Returns the variable map of interpolated definitions. | |
| 133 | * | |
| 134 | * @return A map to help dereference variables. | |
| 135 | */ | |
| 136 | private Map<String, String> getResolvedMap() { | |
| 137 | return mResolvedMap; | |
| 138 | } | |
| 139 | ||
| 140 | /** | |
| 141 | * Returns a processor common to all processors: markdown, caret position | |
| 142 | * token replacer, and an HTML preview renderer. | |
| 143 | * | |
| 144 | * @return Processors at the end of the processing chain. | |
| 145 | */ | |
| 146 | private Processor<String> getCommonProcessor() { | |
| 147 | return mMarkdownProcessor; | |
| 148 | } | |
| 149 | } | |
| 150 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | import com.scrivenvar.sigils.RSigilOperator; | |
| 31 | ||
| 32 | import java.util.HashMap; | |
| 33 | import java.util.Map; | |
| 34 | ||
| 35 | /** | |
| 36 | * Converts the keys of the resolved map from default form to R form, then | |
| 37 | * performs a substitution on the text. The default R variable syntax is | |
| 38 | * {@code v$tree$leaf}. | |
| 39 | */ | |
| 40 | public class RVariableProcessor extends DefinitionProcessor { | |
| 41 | ||
| 42 | public RVariableProcessor( | |
| 43 | final Processor<String> rp, final Map<String, String> map ) { | |
| 44 | super( rp, map ); | |
| 45 | } | |
| 46 | ||
| 47 | /** | |
| 48 | * Returns the R-based version of the interpolated variable definitions. | |
| 49 | * | |
| 50 | * @return Variable names transmogrified from the default syntax to R syntax. | |
| 51 | */ | |
| 52 | @Override | |
| 53 | protected Map<String, String> getDefinitions() { | |
| 54 | return toR( super.getDefinitions() ); | |
| 55 | } | |
| 56 | ||
| 57 | /** | |
| 58 | * Converts the given map from regular variables to R variables. | |
| 59 | * | |
| 60 | * @param map Map of variable names to values. | |
| 61 | * @return Map of R variables. | |
| 62 | */ | |
| 63 | private Map<String, String> toR( final Map<String, String> map ) { | |
| 64 | final var rMap = new HashMap<String, String>( map.size() ); | |
| 65 | ||
| 66 | for( final var entry : map.entrySet() ) { | |
| 67 | final var key = entry.getKey(); | |
| 68 | rMap.put( RSigilOperator.entoken( key ), toRValue( map.get( key ) ) ); | |
| 69 | } | |
| 70 | ||
| 71 | return rMap; | |
| 72 | } | |
| 73 | ||
| 74 | private String toRValue( final String value ) { | |
| 75 | return '\'' + escape( value, '\'', "\\'" ) + '\''; | |
| 76 | } | |
| 77 | ||
| 78 | /** | |
| 79 | * TODO: Make generic method for replacing text. | |
| 80 | * | |
| 81 | * @param haystack Search this string for the needle, must not be null. | |
| 82 | * @param needle The character to find in the haystack. | |
| 83 | * @param thread Replace the needle with this text, if the needle is found. | |
| 84 | * @return The haystack with the all instances of needle replaced with thread. | |
| 85 | */ | |
| 86 | @SuppressWarnings("SameParameterValue") | |
| 87 | private String escape( | |
| 88 | final String haystack, final char needle, final String thread ) { | |
| 89 | int end = haystack.indexOf( needle ); | |
| 90 | ||
| 91 | if( end < 0 ) { | |
| 92 | return haystack; | |
| 93 | } | |
| 94 | ||
| 95 | final int length = haystack.length(); | |
| 96 | int start = 0; | |
| 97 | ||
| 98 | // Replace up to 32 occurrences before the string reallocates its buffer. | |
| 99 | final StringBuilder sb = new StringBuilder( length + 32 ); | |
| 100 | ||
| 101 | while( end >= 0 ) { | |
| 102 | sb.append( haystack, start, end ).append( thread ); | |
| 103 | start = end + 1; | |
| 104 | end = haystack.indexOf( needle, start ); | |
| 105 | } | |
| 106 | ||
| 107 | return sb.append( haystack.substring( start ) ).toString(); | |
| 108 | } | |
| 109 | } | |
| 110 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors; | |
| 29 | ||
| 30 | import com.scrivenvar.Services; | |
| 31 | import com.scrivenvar.service.Snitch; | |
| 32 | import net.sf.saxon.TransformerFactoryImpl; | |
| 33 | import net.sf.saxon.trans.XPathException; | |
| 34 | ||
| 35 | import javax.xml.stream.XMLEventReader; | |
| 36 | import javax.xml.stream.XMLInputFactory; | |
| 37 | import javax.xml.stream.XMLStreamException; | |
| 38 | import javax.xml.stream.events.ProcessingInstruction; | |
| 39 | import javax.xml.stream.events.XMLEvent; | |
| 40 | import javax.xml.transform.*; | |
| 41 | import javax.xml.transform.stream.StreamResult; | |
| 42 | import javax.xml.transform.stream.StreamSource; | |
| 43 | import java.io.File; | |
| 44 | import java.io.Reader; | |
| 45 | import java.io.StringReader; | |
| 46 | import java.io.StringWriter; | |
| 47 | import java.nio.file.Path; | |
| 48 | import java.nio.file.Paths; | |
| 49 | ||
| 50 | import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute; | |
| 51 | ||
| 52 | /** | |
| 53 | * Transforms an XML document. The XML document must have a stylesheet specified | |
| 54 | * as part of its processing instructions, such as: | |
| 55 | * <p> | |
| 56 | * {@code xml-stylesheet type="text/xsl" href="markdown.xsl"} | |
| 57 | * </p> | |
| 58 | * <p> | |
| 59 | * The XSL must transform the XML document into Markdown, or another format | |
| 60 | * recognized by the next link on the chain. | |
| 61 | * </p> | |
| 62 | */ | |
| 63 | public class XmlProcessor extends AbstractProcessor<String> | |
| 64 | implements ErrorListener { | |
| 65 | ||
| 66 | private final Snitch snitch = Services.load( Snitch.class ); | |
| 67 | ||
| 68 | private XMLInputFactory xmlInputFactory; | |
| 69 | private TransformerFactory transformerFactory; | |
| 70 | private Transformer transformer; | |
| 71 | ||
| 72 | private Path path; | |
| 73 | ||
| 74 | /** | |
| 75 | * Constructs an XML processor that can transform an XML document into another | |
| 76 | * format based on the XSL file specified as a processing instruction. The | |
| 77 | * path must point to the directory where the XSL file is found, which implies | |
| 78 | * that they must be in the same directory. | |
| 79 | * | |
| 80 | * @param processor Next link in the processing chain. | |
| 81 | * @param path The path to the XML file content to be processed. | |
| 82 | */ | |
| 83 | public XmlProcessor( final Processor<String> processor, final Path path ) { | |
| 84 | super( processor ); | |
| 85 | setPath( path ); | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Transforms the given XML text into another form (typically Markdown). | |
| 90 | * | |
| 91 | * @param text The text to transform, can be empty, cannot be null. | |
| 92 | * @return The transformed text, or empty if text is empty. | |
| 93 | */ | |
| 94 | @Override | |
| 95 | public String apply( final String text ) { | |
| 96 | try { | |
| 97 | return text.isEmpty() ? text : transform( text ); | |
| 98 | } catch( final Exception ex ) { | |
| 99 | throw new RuntimeException( ex ); | |
| 100 | } | |
| 101 | } | |
| 102 | ||
| 103 | /** | |
| 104 | * Performs an XSL transformation on the given XML text. The XML text must | |
| 105 | * have a processing instruction that points to the XSL template file to use | |
| 106 | * for the transformation. | |
| 107 | * | |
| 108 | * @param text The text to transform. | |
| 109 | * @return The transformed text. | |
| 110 | */ | |
| 111 | private String transform( final String text ) throws Exception { | |
| 112 | // Extract the XML stylesheet processing instruction. | |
| 113 | final String template = getXsltFilename( text ); | |
| 114 | final Path xsl = getXslPath( template ); | |
| 115 | ||
| 116 | try( | |
| 117 | final StringWriter output = new StringWriter( text.length() ); | |
| 118 | final StringReader input = new StringReader( text ) ) { | |
| 119 | ||
| 120 | // Listen for external file modification events. | |
| 121 | getSnitch().listen( xsl ); | |
| 122 | ||
| 123 | getTransformer( xsl ).transform( | |
| 124 | new StreamSource( input ), | |
| 125 | new StreamResult( output ) | |
| 126 | ); | |
| 127 | ||
| 128 | return output.toString(); | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * Returns an XSL transformer ready to transform an XML document using the | |
| 134 | * XSLT file specified by the given path. If the path is already known then | |
| 135 | * this will return the associated transformer. | |
| 136 | * | |
| 137 | * @param xsl The path to an XSLT file. | |
| 138 | * @return A transformer that will transform XML documents using the given | |
| 139 | * XSLT file. | |
| 140 | * @throws TransformerConfigurationException Could not instantiate the | |
| 141 | * transformer. | |
| 142 | */ | |
| 143 | private Transformer getTransformer( final Path xsl ) | |
| 144 | throws TransformerConfigurationException { | |
| 145 | if( this.transformer == null ) { | |
| 146 | this.transformer = createTransformer( xsl ); | |
| 147 | } | |
| 148 | ||
| 149 | return this.transformer; | |
| 150 | } | |
| 151 | ||
| 152 | /** | |
| 153 | * Creates a configured transformer ready to run. | |
| 154 | * | |
| 155 | * @param xsl The stylesheet to use for transforming XML documents. | |
| 156 | * @return The edited XML document transformed into another format (usually | |
| 157 | * markdown). | |
| 158 | * @throws TransformerConfigurationException Could not create the transformer. | |
| 159 | */ | |
| 160 | protected Transformer createTransformer( final Path xsl ) | |
| 161 | throws TransformerConfigurationException { | |
| 162 | final Source xslt = new StreamSource( xsl.toFile() ); | |
| 163 | ||
| 164 | return getTransformerFactory().newTransformer( xslt ); | |
| 165 | } | |
| 166 | ||
| 167 | private Path getXslPath( final String filename ) { | |
| 168 | final Path xmlPath = getPath(); | |
| 169 | final File xmlDirectory = xmlPath.toFile().getParentFile(); | |
| 170 | ||
| 171 | return Paths.get( xmlDirectory.getPath(), filename ); | |
| 172 | } | |
| 173 | ||
| 174 | /** | |
| 175 | * Given XML text, this will use a StAX pull reader to obtain the XML | |
| 176 | * stylesheet processing instruction. This will throw a parse exception if the | |
| 177 | * href pseudo-attribute filename value cannot be found. | |
| 178 | * | |
| 179 | * @param xml The XML containing an xml-stylesheet processing instruction. | |
| 180 | * @return The href pseudo-attribute value. | |
| 181 | * @throws XMLStreamException Could not parse the XML file. | |
| 182 | */ | |
| 183 | private String getXsltFilename( final String xml ) | |
| 184 | throws XMLStreamException, XPathException { | |
| 185 | ||
| 186 | String result = ""; | |
| 187 | ||
| 188 | try( final StringReader sr = new StringReader( xml ) ) { | |
| 189 | boolean found = false; | |
| 190 | int count = 0; | |
| 191 | final XMLEventReader reader = createXMLEventReader( sr ); | |
| 192 | ||
| 193 | // If the processing instruction wasn't found in the first 10 lines, | |
| 194 | // fail fast. This should iterate twice through the loop. | |
| 195 | while( !found && reader.hasNext() && count++ < 10 ) { | |
| 196 | final XMLEvent event = reader.nextEvent(); | |
| 197 | ||
| 198 | if( event.isProcessingInstruction() ) { | |
| 199 | final ProcessingInstruction pi = (ProcessingInstruction) event; | |
| 200 | final String target = pi.getTarget(); | |
| 201 | ||
| 202 | if( "xml-stylesheet".equalsIgnoreCase( target ) ) { | |
| 203 | result = getPseudoAttribute( pi.getData(), "href" ); | |
| 204 | found = true; | |
| 205 | } | |
| 206 | } | |
| 207 | } | |
| 208 | } | |
| 209 | ||
| 210 | return result; | |
| 211 | } | |
| 212 | ||
| 213 | private XMLEventReader createXMLEventReader( final Reader reader ) | |
| 214 | throws XMLStreamException { | |
| 215 | return getXMLInputFactory().createXMLEventReader( reader ); | |
| 216 | } | |
| 217 | ||
| 218 | private synchronized XMLInputFactory getXMLInputFactory() { | |
| 219 | if( this.xmlInputFactory == null ) { | |
| 220 | this.xmlInputFactory = createXMLInputFactory(); | |
| 221 | } | |
| 222 | ||
| 223 | return this.xmlInputFactory; | |
| 224 | } | |
| 225 | ||
| 226 | private XMLInputFactory createXMLInputFactory() { | |
| 227 | return XMLInputFactory.newInstance(); | |
| 228 | } | |
| 229 | ||
| 230 | private synchronized TransformerFactory getTransformerFactory() { | |
| 231 | if( this.transformerFactory == null ) { | |
| 232 | this.transformerFactory = createTransformerFactory(); | |
| 233 | } | |
| 234 | ||
| 235 | return this.transformerFactory; | |
| 236 | } | |
| 237 | ||
| 238 | /** | |
| 239 | * Returns a high-performance XSLT 2 transformation engine. | |
| 240 | * | |
| 241 | * @return An XSL transforming engine. | |
| 242 | */ | |
| 243 | private TransformerFactory createTransformerFactory() { | |
| 244 | final TransformerFactory factory = new TransformerFactoryImpl(); | |
| 245 | ||
| 246 | // Bubble problems up to the user interface, rather than standard error. | |
| 247 | factory.setErrorListener( this ); | |
| 248 | ||
| 249 | return factory; | |
| 250 | } | |
| 251 | ||
| 252 | /** | |
| 253 | * Called when the XSL transformer issues a warning. | |
| 254 | * | |
| 255 | * @param ex The problem the transformer encountered. | |
| 256 | */ | |
| 257 | @Override | |
| 258 | public void warning( final TransformerException ex ) { | |
| 259 | throw new RuntimeException( ex ); | |
| 260 | } | |
| 261 | ||
| 262 | /** | |
| 263 | * Called when the XSL transformer issues an error. | |
| 264 | * | |
| 265 | * @param ex The problem the transformer encountered. | |
| 266 | */ | |
| 267 | @Override | |
| 268 | public void error( final TransformerException ex ) { | |
| 269 | throw new RuntimeException( ex ); | |
| 270 | } | |
| 271 | ||
| 272 | /** | |
| 273 | * Called when the XSL transformer issues a fatal error, which is probably | |
| 274 | * a bit over-dramatic a method name. | |
| 275 | * | |
| 276 | * @param ex The problem the transformer encountered. | |
| 277 | */ | |
| 278 | @Override | |
| 279 | public void fatalError( final TransformerException ex ) { | |
| 280 | throw new RuntimeException( ex ); | |
| 281 | } | |
| 282 | ||
| 283 | private void setPath( final Path path ) { | |
| 284 | this.path = path; | |
| 285 | } | |
| 286 | ||
| 287 | private Path getPath() { | |
| 288 | return this.path; | |
| 289 | } | |
| 290 | ||
| 291 | private Snitch getSnitch() { | |
| 292 | return this.snitch; | |
| 293 | } | |
| 294 | } | |
| 295 | 1 |
| 1 | package com.scrivenvar.processors.markdown; | |
| 2 | ||
| 3 | import com.vladsch.flexmark.ast.BlockQuote; | |
| 4 | import com.vladsch.flexmark.ast.ListBlock; | |
| 5 | import com.vladsch.flexmark.html.AttributeProvider; | |
| 6 | import com.vladsch.flexmark.html.AttributeProviderFactory; | |
| 7 | import com.vladsch.flexmark.html.IndependentAttributeProviderFactory; | |
| 8 | import com.vladsch.flexmark.html.renderer.AttributablePart; | |
| 9 | import com.vladsch.flexmark.html.renderer.LinkResolverContext; | |
| 10 | import com.vladsch.flexmark.util.ast.Block; | |
| 11 | import com.vladsch.flexmark.util.ast.Node; | |
| 12 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 13 | import com.vladsch.flexmark.util.html.MutableAttributes; | |
| 14 | import org.jetbrains.annotations.NotNull; | |
| 15 | ||
| 16 | import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX; | |
| 17 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | |
| 18 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 19 | import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT; | |
| 20 | ||
| 21 | /** | |
| 22 | * Responsible for giving most block-level elements a unique identifier | |
| 23 | * attribute. The identifier is used to coordinate scrolling. | |
| 24 | */ | |
| 25 | public class BlockExtension implements HtmlRendererExtension { | |
| 26 | /** | |
| 27 | * Responsible for creating the id attribute. This class is instantiated | |
| 28 | * each time the document is rendered, thereby resetting the count to zero. | |
| 29 | */ | |
| 30 | public static class IdAttributeProvider implements AttributeProvider { | |
| 31 | private int mCount; | |
| 32 | ||
| 33 | private static AttributeProviderFactory createFactory() { | |
| 34 | return new IndependentAttributeProviderFactory() { | |
| 35 | @Override | |
| 36 | public @NotNull AttributeProvider apply( | |
| 37 | @NotNull final LinkResolverContext context ) { | |
| 38 | return new IdAttributeProvider(); | |
| 39 | } | |
| 40 | }; | |
| 41 | } | |
| 42 | ||
| 43 | @Override | |
| 44 | public void setAttributes( @NotNull Node node, | |
| 45 | @NotNull AttributablePart part, | |
| 46 | @NotNull MutableAttributes attributes ) { | |
| 47 | // Blockquotes are troublesome because they can interleave blank lines | |
| 48 | // without having an equivalent blank line in the source document. That | |
| 49 | // is, in Markdown the > symbol on a line by itself will generate a blank | |
| 50 | // line in the resulting document; however, a > symbol in the text editor | |
| 51 | // does not count as a blank line. Resolving this issue is tricky. | |
| 52 | // | |
| 53 | // The CODE_CONTENT represents <code> embedded inside <pre>; both elements | |
| 54 | // enter this method as FencedCodeBlock, but only the <pre> must be | |
| 55 | // uniquely identified (because they are the same line in Markdown). | |
| 56 | // | |
| 57 | if( node instanceof Block && | |
| 58 | !(node instanceof BlockQuote) && | |
| 59 | !(node instanceof ListBlock) && | |
| 60 | (part != CODE_CONTENT) ) { | |
| 61 | attributes.addValue( "id", PARAGRAPH_ID_PREFIX + mCount++ ); | |
| 62 | } | |
| 63 | } | |
| 64 | } | |
| 65 | ||
| 66 | private BlockExtension() { | |
| 67 | } | |
| 68 | ||
| 69 | @Override | |
| 70 | public void extend( final Builder builder, | |
| 71 | @NotNull final String rendererType ) { | |
| 72 | builder.attributeProviderFactory( IdAttributeProvider.createFactory() ); | |
| 73 | } | |
| 74 | ||
| 75 | public static BlockExtension create() { | |
| 76 | return new BlockExtension(); | |
| 77 | } | |
| 78 | ||
| 79 | @Override | |
| 80 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 81 | } | |
| 82 | } | |
| 83 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.markdown; | |
| 29 | ||
| 30 | import com.scrivenvar.preferences.UserPreferences; | |
| 31 | import com.vladsch.flexmark.ast.Image; | |
| 32 | import com.vladsch.flexmark.html.IndependentLinkResolverFactory; | |
| 33 | import com.vladsch.flexmark.html.LinkResolver; | |
| 34 | import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext; | |
| 35 | import com.vladsch.flexmark.html.renderer.LinkStatus; | |
| 36 | import com.vladsch.flexmark.html.renderer.ResolvedLink; | |
| 37 | import com.vladsch.flexmark.util.ast.Node; | |
| 38 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 39 | import org.jetbrains.annotations.NotNull; | |
| 40 | import org.renjin.repackaged.guava.base.Splitter; | |
| 41 | ||
| 42 | import java.io.File; | |
| 43 | import java.io.FileNotFoundException; | |
| 44 | import java.nio.file.Path; | |
| 45 | ||
| 46 | import static com.scrivenvar.StatusBarNotifier.alert; | |
| 47 | import static com.scrivenvar.util.ProtocolResolver.getProtocol; | |
| 48 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | |
| 49 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 50 | import static java.lang.String.format; | |
| 51 | ||
| 52 | /** | |
| 53 | * Responsible for ensuring that images can be rendered relative to a path. | |
| 54 | * This allows images to be located virtually anywhere. | |
| 55 | */ | |
| 56 | public class ImageLinkExtension implements HtmlRendererExtension { | |
| 57 | ||
| 58 | /** | |
| 59 | * Creates an extension capable of using a relative path to embed images. | |
| 60 | * | |
| 61 | * @param path The {@link Path} to the file being edited; the parent path | |
| 62 | * is the starting location of the relative image directory. | |
| 63 | * @return The new {@link ImageLinkExtension}, never {@code null}. | |
| 64 | */ | |
| 65 | public static ImageLinkExtension create( @NotNull final Path path ) { | |
| 66 | return new ImageLinkExtension( path ); | |
| 67 | } | |
| 68 | ||
| 69 | private class Factory extends IndependentLinkResolverFactory { | |
| 70 | @Override | |
| 71 | public @NotNull LinkResolver apply( | |
| 72 | @NotNull final LinkResolverBasicContext context ) { | |
| 73 | return new ImageLinkResolver(); | |
| 74 | } | |
| 75 | } | |
| 76 | ||
| 77 | private class ImageLinkResolver implements LinkResolver { | |
| 78 | private final UserPreferences mUserPref = getUserPreferences(); | |
| 79 | private final File mImagesUserPrefix = mUserPref.getImagesDirectory(); | |
| 80 | private final String mImageExtensions = mUserPref.getImagesOrder(); | |
| 81 | ||
| 82 | public ImageLinkResolver() { | |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * You can also set/clear/modify attributes through | |
| 87 | * {@link ResolvedLink#getAttributes()} and | |
| 88 | * {@link ResolvedLink#getNonNullAttributes()}. | |
| 89 | */ | |
| 90 | @NotNull | |
| 91 | @Override | |
| 92 | public ResolvedLink resolveLink( | |
| 93 | @NotNull final Node node, | |
| 94 | @NotNull final LinkResolverBasicContext context, | |
| 95 | @NotNull final ResolvedLink link ) { | |
| 96 | return node instanceof Image ? resolve( link ) : link; | |
| 97 | } | |
| 98 | ||
| 99 | private ResolvedLink resolve( final ResolvedLink link ) { | |
| 100 | var url = link.getUrl(); | |
| 101 | final var protocol = getProtocol( url ); | |
| 102 | ||
| 103 | try { | |
| 104 | // If the direct file name exists, then use it directly. | |
| 105 | if( (protocol.isFile() && Path.of( url ).toFile().exists()) || | |
| 106 | protocol.isHttp() ) { | |
| 107 | return valid( link, url ); | |
| 108 | } | |
| 109 | } catch( final Exception ignored ) { | |
| 110 | // Try to resolve the image, dynamically. | |
| 111 | } | |
| 112 | ||
| 113 | try { | |
| 114 | final Path imagePrefix = getImagePrefix().toPath(); | |
| 115 | ||
| 116 | // Path to the file being edited. | |
| 117 | Path editPath = getEditPath(); | |
| 118 | ||
| 119 | // If there is no parent path to the file, it means the file has not | |
| 120 | // been saved. Default to using the value from the user's preferences. | |
| 121 | // The user's preferences will be defaulted to a the application's | |
| 122 | // starting directory. | |
| 123 | if( editPath == null ) { | |
| 124 | editPath = imagePrefix; | |
| 125 | } | |
| 126 | else { | |
| 127 | editPath = Path.of( editPath.toString(), imagePrefix.toString() ); | |
| 128 | } | |
| 129 | ||
| 130 | final Path imagePathPrefix = Path.of( editPath.toString(), url ); | |
| 131 | final String suffixes = getImageExtensions(); | |
| 132 | boolean missing = true; | |
| 133 | ||
| 134 | // Iterate over the user's preferred image file type extensions. | |
| 135 | for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) { | |
| 136 | final String imagePath = format( "%s.%s", imagePathPrefix, ext ); | |
| 137 | final File file = new File( imagePath ); | |
| 138 | ||
| 139 | if( file.exists() ) { | |
| 140 | url = file.toString(); | |
| 141 | missing = false; | |
| 142 | break; | |
| 143 | } | |
| 144 | } | |
| 145 | ||
| 146 | if( missing ) { | |
| 147 | throw new FileNotFoundException( imagePathPrefix + ".*" ); | |
| 148 | } | |
| 149 | ||
| 150 | if( protocol.isFile() ) { | |
| 151 | url = "file://" + url; | |
| 152 | } | |
| 153 | ||
| 154 | return valid( link, url ); | |
| 155 | } catch( final Exception ex ) { | |
| 156 | alert( ex ); | |
| 157 | } | |
| 158 | ||
| 159 | return link; | |
| 160 | } | |
| 161 | ||
| 162 | private ResolvedLink valid( final ResolvedLink link, final String url ) { | |
| 163 | return link.withStatus( LinkStatus.VALID ).withUrl( url ); | |
| 164 | } | |
| 165 | ||
| 166 | private File getImagePrefix() { | |
| 167 | return mImagesUserPrefix; | |
| 168 | } | |
| 169 | ||
| 170 | private String getImageExtensions() { | |
| 171 | return mImageExtensions; | |
| 172 | } | |
| 173 | ||
| 174 | private Path getEditPath() { | |
| 175 | return mPath.getParent(); | |
| 176 | } | |
| 177 | } | |
| 178 | ||
| 179 | private final Path mPath; | |
| 180 | ||
| 181 | private ImageLinkExtension( @NotNull final Path path ) { | |
| 182 | mPath = path; | |
| 183 | } | |
| 184 | ||
| 185 | @Override | |
| 186 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 187 | } | |
| 188 | ||
| 189 | @Override | |
| 190 | public void extend( @NotNull final Builder builder, | |
| 191 | @NotNull final String rendererType ) { | |
| 192 | builder.linkResolverFactory( new Factory() ); | |
| 193 | } | |
| 194 | ||
| 195 | private UserPreferences getUserPreferences() { | |
| 196 | return UserPreferences.getInstance(); | |
| 197 | } | |
| 198 | } | |
| 199 | 1 |
| 1 | package com.scrivenvar.processors.markdown; | |
| 2 | ||
| 3 | import com.vladsch.flexmark.ast.Text; | |
| 4 | import com.vladsch.flexmark.html.HtmlWriter; | |
| 5 | import com.vladsch.flexmark.html.renderer.NodeRenderer; | |
| 6 | import com.vladsch.flexmark.html.renderer.NodeRendererContext; | |
| 7 | import com.vladsch.flexmark.html.renderer.NodeRendererFactory; | |
| 8 | import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | |
| 9 | import com.vladsch.flexmark.util.ast.TextCollectingVisitor; | |
| 10 | import com.vladsch.flexmark.util.data.DataHolder; | |
| 11 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 12 | import org.jetbrains.annotations.NotNull; | |
| 13 | import org.jetbrains.annotations.Nullable; | |
| 14 | ||
| 15 | import java.util.LinkedHashMap; | |
| 16 | import java.util.Map; | |
| 17 | import java.util.Set; | |
| 18 | ||
| 19 | import static com.scrivenvar.processors.text.TextReplacementFactory.replace; | |
| 20 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | |
| 21 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 22 | ||
| 23 | /** | |
| 24 | * Responsible for substituting multi-codepoint glyphs with single codepoint | |
| 25 | * glyphs. The text is adorned with ligatures prior to rendering as HTML. | |
| 26 | * This requires a font that supports ligatures. | |
| 27 | * <p> | |
| 28 | * TODO: I18N https://github.com/DaveJarvis/scrivenvar/issues/81 | |
| 29 | * </p> | |
| 30 | */ | |
| 31 | public class LigatureExtension implements HtmlRendererExtension { | |
| 32 | /** | |
| 33 | * Retain insertion order so that ligature substitution uses longer ligatures | |
| 34 | * ahead of shorter ligatures. The word "ruffian" should use the "ffi" | |
| 35 | * ligature, not the "ff" ligature. | |
| 36 | */ | |
| 37 | private static final Map<String, String> LIGATURES = new LinkedHashMap<>(); | |
| 38 | ||
| 39 | static { | |
| 40 | LIGATURES.put( "ffi", "\uFB03" ); | |
| 41 | LIGATURES.put( "ffl", "\uFB04" ); | |
| 42 | LIGATURES.put( "ff", "\uFB00" ); | |
| 43 | LIGATURES.put( "fi", "\uFB01" ); | |
| 44 | LIGATURES.put( "fl", "\uFB02" ); | |
| 45 | LIGATURES.put( "ft", "\uFB05" ); | |
| 46 | LIGATURES.put( "AE", "\u00C6" ); | |
| 47 | LIGATURES.put( "OE", "\u0152" ); | |
| 48 | // "ae", "\u00E6", | |
| 49 | // "oe", "\u0153", | |
| 50 | } | |
| 51 | ||
| 52 | private static class LigatureRenderer implements NodeRenderer { | |
| 53 | private final TextCollectingVisitor mVisitor = new TextCollectingVisitor(); | |
| 54 | ||
| 55 | @SuppressWarnings("unused") | |
| 56 | public LigatureRenderer( final DataHolder options ) { | |
| 57 | } | |
| 58 | ||
| 59 | @Override | |
| 60 | public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | |
| 61 | return Set.of( new NodeRenderingHandler<>( | |
| 62 | Text.class, LigatureRenderer.this::render ) ); | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * This will pick the fastest string replacement algorithm based on the | |
| 67 | * text length. The insertion order of the {@link #LIGATURES} is | |
| 68 | * important to give precedence to longer ligatures. | |
| 69 | * | |
| 70 | * @param textNode The text node containing text to replace with ligatures. | |
| 71 | * @param context Not used. | |
| 72 | * @param html Where to write the text adorned with ligatures. | |
| 73 | */ | |
| 74 | private void render( | |
| 75 | @NotNull final Text textNode, | |
| 76 | @NotNull final NodeRendererContext context, | |
| 77 | @NotNull final HtmlWriter html ) { | |
| 78 | final var text = mVisitor.collectAndGetText( textNode ); | |
| 79 | html.text( replace( text, LIGATURES ) ); | |
| 80 | } | |
| 81 | } | |
| 82 | ||
| 83 | private static class Factory implements NodeRendererFactory { | |
| 84 | @NotNull | |
| 85 | @Override | |
| 86 | public NodeRenderer apply( @NotNull DataHolder options ) { | |
| 87 | return new LigatureRenderer( options ); | |
| 88 | } | |
| 89 | } | |
| 90 | ||
| 91 | private LigatureExtension() { | |
| 92 | } | |
| 93 | ||
| 94 | @Override | |
| 95 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 96 | } | |
| 97 | ||
| 98 | @Override | |
| 99 | public void extend( @NotNull final Builder builder, | |
| 100 | @NotNull final String rendererType ) { | |
| 101 | if( "HTML".equalsIgnoreCase( rendererType ) ) { | |
| 102 | builder.nodeRendererFactory( new Factory() ); | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | public static LigatureExtension create() { | |
| 107 | return new LigatureExtension(); | |
| 108 | } | |
| 109 | } | |
| 110 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.markdown; | |
| 29 | ||
| 30 | import com.scrivenvar.processors.AbstractProcessor; | |
| 31 | import com.scrivenvar.processors.Processor; | |
| 32 | import com.vladsch.flexmark.ext.definition.DefinitionExtension; | |
| 33 | import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension; | |
| 34 | import com.vladsch.flexmark.ext.superscript.SuperscriptExtension; | |
| 35 | import com.vladsch.flexmark.ext.tables.TablesExtension; | |
| 36 | import com.vladsch.flexmark.ext.typographic.TypographicExtension; | |
| 37 | import com.vladsch.flexmark.html.HtmlRenderer; | |
| 38 | import com.vladsch.flexmark.parser.Parser; | |
| 39 | import com.vladsch.flexmark.util.ast.IParse; | |
| 40 | import com.vladsch.flexmark.util.ast.Node; | |
| 41 | import com.vladsch.flexmark.util.misc.Extension; | |
| 42 | ||
| 43 | import java.nio.file.Path; | |
| 44 | import java.util.ArrayList; | |
| 45 | import java.util.Collection; | |
| 46 | ||
| 47 | import static com.scrivenvar.Constants.USER_DIRECTORY; | |
| 48 | ||
| 49 | /** | |
| 50 | * Responsible for parsing a Markdown document and rendering it as HTML. | |
| 51 | */ | |
| 52 | public class MarkdownProcessor extends AbstractProcessor<String> { | |
| 53 | ||
| 54 | private final HtmlRenderer mRenderer; | |
| 55 | private final IParse mParser; | |
| 56 | ||
| 57 | public MarkdownProcessor( | |
| 58 | final Processor<String> successor ) { | |
| 59 | this( successor, Path.of( USER_DIRECTORY ) ); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Constructs a new Markdown processor that can create HTML documents. | |
| 64 | * | |
| 65 | * @param successor Usually the HTML Preview Processor. | |
| 66 | */ | |
| 67 | public MarkdownProcessor( | |
| 68 | final Processor<String> successor, final Path path ) { | |
| 69 | super( successor ); | |
| 70 | ||
| 71 | // Standard extensions | |
| 72 | final Collection<Extension> extensions = new ArrayList<>(); | |
| 73 | extensions.add( DefinitionExtension.create() ); | |
| 74 | extensions.add( StrikethroughSubscriptExtension.create() ); | |
| 75 | extensions.add( SuperscriptExtension.create() ); | |
| 76 | extensions.add( TablesExtension.create() ); | |
| 77 | extensions.add( TypographicExtension.create() ); | |
| 78 | ||
| 79 | // Allows referencing image files via relative paths and dynamic file types. | |
| 80 | extensions.add( ImageLinkExtension.create( path ) ); | |
| 81 | extensions.add( BlockExtension.create() ); | |
| 82 | extensions.add( TeXExtension.create() ); | |
| 83 | ||
| 84 | // TODO: https://github.com/FAlthausen/Vollkorn-Typeface/issues/38 | |
| 85 | // TODO: Uncomment when Vollkorn ligatures are fixed. | |
| 86 | // extensions.add( LigatureExtension.create() ); | |
| 87 | ||
| 88 | mRenderer = HtmlRenderer.builder().extensions( extensions ).build(); | |
| 89 | mParser = Parser.builder() | |
| 90 | .extensions( extensions ) | |
| 91 | .build(); | |
| 92 | } | |
| 93 | ||
| 94 | /** | |
| 95 | * Converts the given Markdown string into HTML, without the doctype, html, | |
| 96 | * head, and body tags. | |
| 97 | * | |
| 98 | * @param markdown The string to convert from Markdown to HTML. | |
| 99 | * @return The HTML representation of the Markdown document. | |
| 100 | */ | |
| 101 | @Override | |
| 102 | public String apply( final String markdown ) { | |
| 103 | return toHtml( markdown ); | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Returns the AST in the form of a node for the given markdown document. This | |
| 108 | * can be used, for example, to determine if a hyperlink exists inside of a | |
| 109 | * paragraph. | |
| 110 | * | |
| 111 | * @param markdown The markdown to convert into an AST. | |
| 112 | * @return The markdown AST for the given text (usually a paragraph). | |
| 113 | */ | |
| 114 | public Node toNode( final String markdown ) { | |
| 115 | return parse( markdown ); | |
| 116 | } | |
| 117 | ||
| 118 | /** | |
| 119 | * Helper method to create an AST given some markdown. | |
| 120 | * | |
| 121 | * @param markdown The markdown to parse. | |
| 122 | * @return The root node of the markdown tree. | |
| 123 | */ | |
| 124 | private Node parse( final String markdown ) { | |
| 125 | return getParser().parse( markdown ); | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Converts a string of markdown into HTML. | |
| 130 | * | |
| 131 | * @param markdown The markdown text to convert to HTML, must not be null. | |
| 132 | * @return The markdown rendered as an HTML document. | |
| 133 | */ | |
| 134 | private String toHtml( final String markdown ) { | |
| 135 | return getRenderer().render( parse( markdown ) ); | |
| 136 | } | |
| 137 | ||
| 138 | /** | |
| 139 | * Creates the Markdown document processor. | |
| 140 | * | |
| 141 | * @return A Parser that can build an abstract syntax tree. | |
| 142 | */ | |
| 143 | private IParse getParser() { | |
| 144 | return mParser; | |
| 145 | } | |
| 146 | ||
| 147 | private HtmlRenderer getRenderer() { | |
| 148 | return mRenderer; | |
| 149 | } | |
| 150 | } | |
| 151 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.markdown; | |
| 29 | ||
| 30 | import com.scrivenvar.processors.markdown.tex.TeXInlineDelimiterProcessor; | |
| 31 | import com.scrivenvar.processors.markdown.tex.TeXNodeRenderer; | |
| 32 | import com.vladsch.flexmark.html.HtmlRenderer; | |
| 33 | import com.vladsch.flexmark.parser.Parser; | |
| 34 | import com.vladsch.flexmark.util.data.MutableDataHolder; | |
| 35 | import org.jetbrains.annotations.NotNull; | |
| 36 | ||
| 37 | import static com.vladsch.flexmark.html.HtmlRenderer.HtmlRendererExtension; | |
| 38 | import static com.vladsch.flexmark.parser.Parser.ParserExtension; | |
| 39 | ||
| 40 | /** | |
| 41 | * Responsible for wrapping delimited TeX code in Markdown into an XML element | |
| 42 | * that the HTML renderer can handle. For example, {@code $E=mc^2$} becomes | |
| 43 | * {@code <tex>E=mc^2</tex>} when passed to HTML renderer. The HTML renderer | |
| 44 | * is responsible for converting the TeX code for display. This avoids inserting | |
| 45 | * SVG code into the Markdown document, which the parser would then have to | |
| 46 | * iterate---a <em>very</em> wasteful operation that impacts front-end | |
| 47 | * performance. | |
| 48 | */ | |
| 49 | public class TeXExtension implements ParserExtension, HtmlRendererExtension { | |
| 50 | /** | |
| 51 | * Creates an extension capable of handling delimited TeX code in Markdown. | |
| 52 | * | |
| 53 | * @return The new {@link TeXExtension}, never {@code null}. | |
| 54 | */ | |
| 55 | public static TeXExtension create() { | |
| 56 | return new TeXExtension(); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Force using the {@link #create()} method for consistency. | |
| 61 | */ | |
| 62 | private TeXExtension() { | |
| 63 | } | |
| 64 | ||
| 65 | /** | |
| 66 | * Adds the TeX extension for HTML document export types. | |
| 67 | * | |
| 68 | * @param builder The document builder. | |
| 69 | * @param rendererType Indicates the document type to be built. | |
| 70 | */ | |
| 71 | @Override | |
| 72 | public void extend( @NotNull final HtmlRenderer.Builder builder, | |
| 73 | @NotNull final String rendererType ) { | |
| 74 | if( "HTML".equalsIgnoreCase( rendererType ) ) { | |
| 75 | builder.nodeRendererFactory( new TeXNodeRenderer.Factory() ); | |
| 76 | } | |
| 77 | } | |
| 78 | ||
| 79 | @Override | |
| 80 | public void extend( final Parser.Builder builder ) { | |
| 81 | builder.customDelimiterProcessor( new TeXInlineDelimiterProcessor() ); | |
| 82 | } | |
| 83 | ||
| 84 | @Override | |
| 85 | public void rendererOptions( @NotNull final MutableDataHolder options ) { | |
| 86 | } | |
| 87 | ||
| 88 | @Override | |
| 89 | public void parserOptions( final MutableDataHolder options ) { | |
| 90 | } | |
| 91 | } | |
| 92 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.markdown.tex; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.parser.InlineParser; | |
| 31 | import com.vladsch.flexmark.parser.core.delimiter.Delimiter; | |
| 32 | import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor; | |
| 33 | import com.vladsch.flexmark.parser.delimiter.DelimiterRun; | |
| 34 | import com.vladsch.flexmark.util.ast.Node; | |
| 35 | ||
| 36 | public class TeXInlineDelimiterProcessor implements DelimiterProcessor { | |
| 37 | ||
| 38 | @Override | |
| 39 | public void process( final Delimiter opener, final Delimiter closer, | |
| 40 | final int delimitersUsed ) { | |
| 41 | final var node = new TeXNode(); | |
| 42 | opener.moveNodesBetweenDelimitersTo(node, closer); | |
| 43 | } | |
| 44 | ||
| 45 | @Override | |
| 46 | public char getOpeningCharacter() { | |
| 47 | return '$'; | |
| 48 | } | |
| 49 | ||
| 50 | @Override | |
| 51 | public char getClosingCharacter() { | |
| 52 | return '$'; | |
| 53 | } | |
| 54 | ||
| 55 | @Override | |
| 56 | public int getMinLength() { | |
| 57 | return 1; | |
| 58 | } | |
| 59 | ||
| 60 | /** | |
| 61 | * Allow for $ or $$. | |
| 62 | * | |
| 63 | * @param opener One or more opening delimiter characters. | |
| 64 | * @param closer One or more closing delimiter characters. | |
| 65 | * @return The number of delimiters to use to determine whether a valid | |
| 66 | * opening delimiter expression is found. | |
| 67 | */ | |
| 68 | @Override | |
| 69 | public int getDelimiterUse( | |
| 70 | final DelimiterRun opener, final DelimiterRun closer ) { | |
| 71 | return 1; | |
| 72 | } | |
| 73 | ||
| 74 | @Override | |
| 75 | public boolean canBeOpener( final String before, | |
| 76 | final String after, | |
| 77 | final boolean leftFlanking, | |
| 78 | final boolean rightFlanking, | |
| 79 | final boolean beforeIsPunctuation, | |
| 80 | final boolean afterIsPunctuation, | |
| 81 | final boolean beforeIsWhitespace, | |
| 82 | final boolean afterIsWhiteSpace ) { | |
| 83 | return leftFlanking; | |
| 84 | } | |
| 85 | ||
| 86 | @Override | |
| 87 | public boolean canBeCloser( final String before, | |
| 88 | final String after, | |
| 89 | final boolean leftFlanking, | |
| 90 | final boolean rightFlanking, | |
| 91 | final boolean beforeIsPunctuation, | |
| 92 | final boolean afterIsPunctuation, | |
| 93 | final boolean beforeIsWhitespace, | |
| 94 | final boolean afterIsWhiteSpace ) { | |
| 95 | return rightFlanking; | |
| 96 | } | |
| 97 | ||
| 98 | @Override | |
| 99 | public Node unmatchedDelimiterNode( | |
| 100 | final InlineParser inlineParser, final DelimiterRun delimiter ) { | |
| 101 | return null; | |
| 102 | } | |
| 103 | ||
| 104 | @Override | |
| 105 | public boolean skipNonOpenerCloser() { | |
| 106 | return false; | |
| 107 | } | |
| 108 | } | |
| 109 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.markdown.tex; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.ast.DelimitedNodeImpl; | |
| 31 | ||
| 32 | public class TeXNode extends DelimitedNodeImpl { | |
| 33 | /** | |
| 34 | * TeX expression wrapped in a {@code <tex>} element. | |
| 35 | */ | |
| 36 | public static final String HTML_TEX = "tex"; | |
| 37 | ||
| 38 | public TeXNode() { | |
| 39 | } | |
| 40 | } | |
| 41 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.markdown.tex; | |
| 29 | ||
| 30 | import com.vladsch.flexmark.html.HtmlWriter; | |
| 31 | import com.vladsch.flexmark.html.renderer.NodeRenderer; | |
| 32 | import com.vladsch.flexmark.html.renderer.NodeRendererContext; | |
| 33 | import com.vladsch.flexmark.html.renderer.NodeRendererFactory; | |
| 34 | import com.vladsch.flexmark.html.renderer.NodeRenderingHandler; | |
| 35 | import com.vladsch.flexmark.util.data.DataHolder; | |
| 36 | import org.jetbrains.annotations.NotNull; | |
| 37 | import org.jetbrains.annotations.Nullable; | |
| 38 | ||
| 39 | import java.util.Set; | |
| 40 | ||
| 41 | import static com.scrivenvar.processors.markdown.tex.TeXNode.HTML_TEX; | |
| 42 | ||
| 43 | public class TeXNodeRenderer implements NodeRenderer { | |
| 44 | ||
| 45 | public static class Factory implements NodeRendererFactory { | |
| 46 | @NotNull | |
| 47 | @Override | |
| 48 | public NodeRenderer apply( @NotNull DataHolder options ) { | |
| 49 | return new TeXNodeRenderer(); | |
| 50 | } | |
| 51 | } | |
| 52 | ||
| 53 | @Override | |
| 54 | public @Nullable Set<NodeRenderingHandler<?>> getNodeRenderingHandlers() { | |
| 55 | return Set.of( new NodeRenderingHandler<>( TeXNode.class, this::render ) ); | |
| 56 | } | |
| 57 | ||
| 58 | private void render( final TeXNode node, | |
| 59 | final NodeRendererContext context, | |
| 60 | final HtmlWriter html ) { | |
| 61 | html.tag( HTML_TEX ); | |
| 62 | html.raw( node.getText() ); | |
| 63 | html.closeTag( HTML_TEX ); | |
| 64 | } | |
| 65 | } | |
| 66 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for common behaviour across all text replacer implementations. | |
| 34 | */ | |
| 35 | public abstract class AbstractTextReplacer implements TextReplacer { | |
| 36 | ||
| 37 | /** | |
| 38 | * Default (empty) constructor. | |
| 39 | */ | |
| 40 | protected AbstractTextReplacer() { | |
| 41 | } | |
| 42 | ||
| 43 | protected String[] keys( final Map<String, String> map ) { | |
| 44 | return map.keySet().toArray( new String[ 0 ] ); | |
| 45 | } | |
| 46 | ||
| 47 | protected String[] values( final Map<String, String> map ) { | |
| 48 | return map.values().toArray( new String[ 0 ] ); | |
| 49 | } | |
| 50 | } | |
| 51 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | import org.ahocorasick.trie.Emit; | |
| 32 | import org.ahocorasick.trie.Trie.TrieBuilder; | |
| 33 | import static org.ahocorasick.trie.Trie.builder; | |
| 34 | ||
| 35 | /** | |
| 36 | * Replaces text using an Aho-Corasick algorithm. | |
| 37 | */ | |
| 38 | public class AhoCorasickReplacer extends AbstractTextReplacer { | |
| 39 | ||
| 40 | /** | |
| 41 | * Default (empty) constructor. | |
| 42 | */ | |
| 43 | protected AhoCorasickReplacer() { | |
| 44 | } | |
| 45 | ||
| 46 | @Override | |
| 47 | public String replace( final String text, final Map<String, String> map ) { | |
| 48 | // Create a buffer sufficiently large that re-allocations are minimized. | |
| 49 | final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) ); | |
| 50 | ||
| 51 | // The TrieBuilder should only match whole words and ignore overlaps (there | |
| 52 | // shouldn't be any). | |
| 53 | final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps(); | |
| 54 | ||
| 55 | for( final String key : keys( map ) ) { | |
| 56 | builder.addKeyword( key ); | |
| 57 | } | |
| 58 | ||
| 59 | int index = 0; | |
| 60 | ||
| 61 | // Replace all instances with dereferenced variables. | |
| 62 | for( final Emit emit : builder.build().parseText( text ) ) { | |
| 63 | sb.append( text, index, emit.getStart() ); | |
| 64 | sb.append( map.get( emit.getKeyword() ) ); | |
| 65 | index = emit.getEnd() + 1; | |
| 66 | } | |
| 67 | ||
| 68 | // Add the remainder of the string (contains no more matches). | |
| 69 | sb.append( text.substring( index ) ); | |
| 70 | ||
| 71 | return sb.toString(); | |
| 72 | } | |
| 73 | } | |
| 74 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | import static org.apache.commons.lang3.StringUtils.replaceEach; | |
| 33 | ||
| 34 | /** | |
| 35 | * Replaces text using Apache's StringUtils.replaceEach method. | |
| 36 | */ | |
| 37 | public class StringUtilsReplacer extends AbstractTextReplacer { | |
| 38 | ||
| 39 | /** | |
| 40 | * Default (empty) constructor. | |
| 41 | */ | |
| 42 | protected StringUtilsReplacer() { | |
| 43 | } | |
| 44 | ||
| 45 | @Override | |
| 46 | public String replace( final String text, final Map<String, String> map ) { | |
| 47 | return replaceEach( text, keys( map ), values( map ) ); | |
| 48 | } | |
| 49 | } | |
| 50 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | /** | |
| 33 | * Used to generate a class capable of efficiently replacing variable | |
| 34 | * definitions with their values. | |
| 35 | */ | |
| 36 | public final class TextReplacementFactory { | |
| 37 | ||
| 38 | private static final TextReplacer APACHE = new StringUtilsReplacer(); | |
| 39 | private static final TextReplacer AHO_CORASICK = new AhoCorasickReplacer(); | |
| 40 | ||
| 41 | /** | |
| 42 | * Returns a text search/replacement instance that is reasonably optimal for | |
| 43 | * the given length of text. | |
| 44 | * | |
| 45 | * @param length The length of text that requires some search and replacing. | |
| 46 | * @return A class that can search and replace text with utmost expediency. | |
| 47 | */ | |
| 48 | public static TextReplacer getTextReplacer( final int length ) { | |
| 49 | // After about 1,500 characters, the StringUtils implementation is less | |
| 50 | // performant than the Aho-Corsick implementation. | |
| 51 | // | |
| 52 | // See http://stackoverflow.com/a/40836618/59087 | |
| 53 | return length < 1500 ? APACHE : AHO_CORASICK; | |
| 54 | } | |
| 55 | ||
| 56 | /** | |
| 57 | * Convenience method to instantiate a suitable text replacer algorithm and | |
| 58 | * perform a replacement using the given map. At this point, the values should | |
| 59 | * be already dereferenced and ready to be substituted verbatim; any | |
| 60 | * recursively defined values must have been interpolated previously. | |
| 61 | * | |
| 62 | * @param text The text containing zero or more variables to replace. | |
| 63 | * @param map The map of variables to their dereferenced values. | |
| 64 | * @return The text with all variables replaced. | |
| 65 | */ | |
| 66 | public static String replace( | |
| 67 | final String text, final Map<String, String> map ) { | |
| 68 | return getTextReplacer( text.length() ).replace( text, map ); | |
| 69 | } | |
| 70 | } | |
| 71 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.processors.text; | |
| 29 | ||
| 30 | import java.util.Map; | |
| 31 | ||
| 32 | /** | |
| 33 | * Defines the ability to replace text given a set of keys and values. | |
| 34 | */ | |
| 35 | public interface TextReplacer { | |
| 36 | ||
| 37 | /** | |
| 38 | * Searches through the given text for any of the keys given in the map and | |
| 39 | * replaces the keys that appear in the text with the key's corresponding | |
| 40 | * value. | |
| 41 | * | |
| 42 | * @param text The text that contains zero or more keys. | |
| 43 | * @param map The set of keys mapped to replacement values. | |
| 44 | * @return The given text with all keys replaced with corresponding values. | |
| 45 | */ | |
| 46 | String replace( String text, Map<String, String> map ); | |
| 47 | } | |
| 48 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service; | |
| 29 | ||
| 30 | import com.dlsc.preferencesfx.PreferencesFx; | |
| 31 | ||
| 32 | import java.util.prefs.BackingStoreException; | |
| 33 | import java.util.prefs.Preferences; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for persisting options that are safe to load before the UI | |
| 37 | * is shown. This can include items like window dimensions, last file | |
| 38 | * opened, split pane locations, and more. This cannot be used to persist | |
| 39 | * options that are user-controlled (i.e., all options available through | |
| 40 | * {@link PreferencesFx}). | |
| 41 | */ | |
| 42 | public interface Options extends Service { | |
| 43 | ||
| 44 | /** | |
| 45 | * Returns the {@link Preferences} that persist settings that cannot | |
| 46 | * be configured via the user interface. | |
| 47 | * | |
| 48 | * @return A valid {@link Preferences} instance, never {@code null}. | |
| 49 | */ | |
| 50 | Preferences getState(); | |
| 51 | ||
| 52 | /** | |
| 53 | * Stores the key and value into the user preferences to be loaded the next | |
| 54 | * time the application is launched. | |
| 55 | * | |
| 56 | * @param key Name of the key to persist along with its value. | |
| 57 | * @param value Value to associate with the key. | |
| 58 | * @throws BackingStoreException Could not persist the change. | |
| 59 | */ | |
| 60 | void put( String key, String value ) throws BackingStoreException; | |
| 61 | ||
| 62 | /** | |
| 63 | * Retrieves the value for a key in the user preferences. | |
| 64 | * | |
| 65 | * @param key Retrieve the value of this key. | |
| 66 | * @param defaultValue The value to return in the event that the given key has | |
| 67 | * no associated value. | |
| 68 | * @return The value associated with the key. | |
| 69 | */ | |
| 70 | String get( String key, String defaultValue ); | |
| 71 | ||
| 72 | /** | |
| 73 | * Retrieves the value for a key in the user preferences. This will return | |
| 74 | * the empty string if the value cannot be found. | |
| 75 | * | |
| 76 | * @param key The key to find in the preferences. | |
| 77 | * @return A non-null, possibly empty value for the key. | |
| 78 | */ | |
| 79 | String get( String key ); | |
| 80 | } | |
| 81 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service; | |
| 29 | ||
| 30 | /** | |
| 31 | * All services inherit from this one. | |
| 32 | */ | |
| 33 | public interface Service { | |
| 34 | } | |
| 35 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service; | |
| 29 | ||
| 30 | import java.util.Iterator; | |
| 31 | import java.util.List; | |
| 32 | ||
| 33 | /** | |
| 34 | * Defines how settings and options can be retrieved. | |
| 35 | */ | |
| 36 | public interface Settings extends Service { | |
| 37 | ||
| 38 | /** | |
| 39 | * Returns a setting property or its default value. | |
| 40 | * | |
| 41 | * @param property The property key name to obtain its value. | |
| 42 | * @param defaultValue The default value to return iff the property cannot be | |
| 43 | * found. | |
| 44 | * @return The property value for the given property key. | |
| 45 | */ | |
| 46 | String getSetting( String property, String defaultValue ); | |
| 47 | ||
| 48 | /** | |
| 49 | * Returns a setting property or its default value. | |
| 50 | * | |
| 51 | * @param property The property key name to obtain its value. | |
| 52 | * @param defaultValue The default value to return iff the property cannot be | |
| 53 | * found. | |
| 54 | * @return The property value for the given property key. | |
| 55 | */ | |
| 56 | int getSetting( String property, int defaultValue ); | |
| 57 | ||
| 58 | /** | |
| 59 | * Returns a list of property names that begin with the given prefix. The | |
| 60 | * prefix is included in any matching results. This will return keys that | |
| 61 | * either match the prefix or start with the prefix followed by a dot ('.'). | |
| 62 | * For example, a prefix value of <code>the.property.name</code> will likely | |
| 63 | * return the expected results, but <code>the.property.name.</code> (note the | |
| 64 | * extraneous period) will probably not. | |
| 65 | * | |
| 66 | * @param prefix The prefix to compare against each property name. | |
| 67 | * @return The list of property names that have the given prefix. | |
| 68 | */ | |
| 69 | Iterator<String> getKeys( final String prefix ); | |
| 70 | ||
| 71 | /** | |
| 72 | * Convert the generic list of property objects into strings. | |
| 73 | * | |
| 74 | * @param property The property value to coerce. | |
| 75 | * @param defaults The defaults values to use should the property be unset. | |
| 76 | * @return The list of properties coerced from objects to strings. | |
| 77 | */ | |
| 78 | List<String> getStringSettingList( String property, List<String> defaults ); | |
| 79 | ||
| 80 | /** | |
| 81 | * Converts the generic list of property objects into strings. | |
| 82 | * | |
| 83 | * @param property The property value to coerce. | |
| 84 | * @return The list of properties coerced from objects to strings. | |
| 85 | */ | |
| 86 | List<String> getStringSettingList( String property ); | |
| 87 | } | |
| 88 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service; | |
| 29 | ||
| 30 | import java.io.IOException; | |
| 31 | import java.nio.file.Path; | |
| 32 | import java.util.Observer; | |
| 33 | ||
| 34 | /** | |
| 35 | * Listens for changes to file system files and directories. | |
| 36 | */ | |
| 37 | public interface Snitch extends Service, Runnable { | |
| 38 | ||
| 39 | /** | |
| 40 | * Adds an observer to the set of observers for this object, provided that it | |
| 41 | * is not the same as some observer already in the set. The order in which | |
| 42 | * notifications will be delivered to multiple observers is not specified. | |
| 43 | * | |
| 44 | * @param o The object to receive changed events for when monitored files | |
| 45 | * are changed. | |
| 46 | */ | |
| 47 | void addObserver( Observer o ); | |
| 48 | ||
| 49 | /** | |
| 50 | * Listens for changes to the path. If the path specifies a file, then only | |
| 51 | * notifications pertaining to that file are sent. Otherwise, change events | |
| 52 | * for the directory that contains the file are sent. This method must allow | |
| 53 | * for multiple calls to the same file without incurring additional listeners | |
| 54 | * or events. | |
| 55 | * | |
| 56 | * @param file Send notifications when this file changes, can be null. | |
| 57 | * @throws IOException Couldn't create a watcher for the given file. | |
| 58 | */ | |
| 59 | void listen( Path file ) throws IOException; | |
| 60 | ||
| 61 | /** | |
| 62 | * Removes the given file from the notifications list. | |
| 63 | * | |
| 64 | * @param file The file to stop monitoring for any changes, can be null. | |
| 65 | */ | |
| 66 | void ignore( Path file ); | |
| 67 | ||
| 68 | /** | |
| 69 | * Stop listening for events. | |
| 70 | */ | |
| 71 | void stop(); | |
| 72 | } | |
| 73 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service.events; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents a message that contains a title and content. | |
| 32 | */ | |
| 33 | public interface Notification { | |
| 34 | ||
| 35 | /** | |
| 36 | * Alert title. | |
| 37 | * | |
| 38 | * @return A non-null string to use as alert message title. | |
| 39 | */ | |
| 40 | String getTitle(); | |
| 41 | ||
| 42 | /** | |
| 43 | * Alert message content. | |
| 44 | * | |
| 45 | * @return A non-null string that contains information for the user. | |
| 46 | */ | |
| 47 | String getContent(); | |
| 48 | } | |
| 49 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service.events; | |
| 29 | ||
| 30 | import javafx.scene.control.Alert; | |
| 31 | import javafx.scene.control.ButtonType; | |
| 32 | import javafx.stage.Window; | |
| 33 | ||
| 34 | /** | |
| 35 | * Provides the application with a uniform way to notify the user of events. | |
| 36 | */ | |
| 37 | public interface Notifier { | |
| 38 | ||
| 39 | ButtonType YES = ButtonType.YES; | |
| 40 | ButtonType NO = ButtonType.NO; | |
| 41 | ButtonType CANCEL = ButtonType.CANCEL; | |
| 42 | ||
| 43 | /** | |
| 44 | * Constructs a default alert message text for a modal alert dialog. | |
| 45 | * | |
| 46 | * @param title The dialog box message title. | |
| 47 | * @param message The dialog box message content (needs formatting). | |
| 48 | * @param args The arguments to the message content that must be formatted. | |
| 49 | * @return The message suitable for building a modal alert dialog. | |
| 50 | */ | |
| 51 | Notification createNotification( | |
| 52 | String title, | |
| 53 | String message, | |
| 54 | Object... args ); | |
| 55 | ||
| 56 | /** | |
| 57 | * Creates an alert of alert type error with a message showing the cause of | |
| 58 | * the error. | |
| 59 | * | |
| 60 | * @param parent Dialog box owner (for modal purposes). | |
| 61 | * @param message The error message, title, and possibly more details. | |
| 62 | * @return A modal alert dialog box ready to display using showAndWait. | |
| 63 | */ | |
| 64 | Alert createError( Window parent, Notification message ); | |
| 65 | ||
| 66 | /** | |
| 67 | * Creates an alert of alert type confirmation with Yes/No/Cancel buttons. | |
| 68 | * | |
| 69 | * @param parent Dialog box owner (for modal purposes). | |
| 70 | * @param message The message, title, and possibly more details. | |
| 71 | * @return A modal alert dialog box ready to display using showAndWait. | |
| 72 | */ | |
| 73 | Alert createConfirmation( Window parent, Notification message ); | |
| 74 | } | |
| 75 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service.events.impl; | |
| 29 | ||
| 30 | import javafx.scene.Node; | |
| 31 | import javafx.scene.control.ButtonBar; | |
| 32 | import javafx.scene.control.DialogPane; | |
| 33 | ||
| 34 | import static com.scrivenvar.Constants.SETTINGS; | |
| 35 | import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS; | |
| 36 | ||
| 37 | /** | |
| 38 | * Ensures a consistent button order for alert dialogs across platforms (because | |
| 39 | * the default button order on Linux defies all logic). | |
| 40 | */ | |
| 41 | public class ButtonOrderPane extends DialogPane { | |
| 42 | ||
| 43 | @Override | |
| 44 | protected Node createButtonBar() { | |
| 45 | final var node = (ButtonBar) super.createButtonBar(); | |
| 46 | node.setButtonOrder( getButtonOrder() ); | |
| 47 | return node; | |
| 48 | } | |
| 49 | ||
| 50 | private String getButtonOrder() { | |
| 51 | return getSetting( "dialog.alert.button.order.windows", | |
| 52 | BUTTON_ORDER_WINDOWS ); | |
| 53 | } | |
| 54 | ||
| 55 | @SuppressWarnings("SameParameterValue") | |
| 56 | private String getSetting( final String key, final String defaultValue ) { | |
| 57 | return SETTINGS.getSetting( key, defaultValue ); | |
| 58 | } | |
| 59 | } | |
| 60 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service.events.impl; | |
| 29 | ||
| 30 | import com.scrivenvar.service.events.Notification; | |
| 31 | ||
| 32 | import java.text.MessageFormat; | |
| 33 | ||
| 34 | /** | |
| 35 | * Responsible for alerting the user to prominent information. | |
| 36 | */ | |
| 37 | public class DefaultNotification implements Notification { | |
| 38 | ||
| 39 | private final String title; | |
| 40 | private final String content; | |
| 41 | ||
| 42 | /** | |
| 43 | * Constructs default message text for a notification. | |
| 44 | * | |
| 45 | * @param title The message title. | |
| 46 | * @param message The message content (needs formatting). | |
| 47 | * @param args The arguments to the message content that must be formatted. | |
| 48 | */ | |
| 49 | public DefaultNotification( | |
| 50 | final String title, | |
| 51 | final String message, | |
| 52 | final Object... args ) { | |
| 53 | this.title = title; | |
| 54 | this.content = MessageFormat.format( message, args ); | |
| 55 | } | |
| 56 | ||
| 57 | @Override | |
| 58 | public String getTitle() { | |
| 59 | return this.title; | |
| 60 | } | |
| 61 | ||
| 62 | @Override | |
| 63 | public String getContent() { | |
| 64 | return this.content; | |
| 65 | } | |
| 66 | } | |
| 67 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service.events.impl; | |
| 29 | ||
| 30 | import com.scrivenvar.service.events.Notification; | |
| 31 | import com.scrivenvar.service.events.Notifier; | |
| 32 | import javafx.scene.control.Alert; | |
| 33 | import javafx.scene.control.Alert.AlertType; | |
| 34 | import javafx.stage.Window; | |
| 35 | ||
| 36 | import static javafx.scene.control.Alert.AlertType.CONFIRMATION; | |
| 37 | import static javafx.scene.control.Alert.AlertType.ERROR; | |
| 38 | ||
| 39 | /** | |
| 40 | * Provides the ability to notify the user of events that need attention, | |
| 41 | * such as prompting the user to confirm closing when there are unsaved changes. | |
| 42 | */ | |
| 43 | public final class DefaultNotifier implements Notifier { | |
| 44 | ||
| 45 | /** | |
| 46 | * Contains all the information that the user needs to know about a problem. | |
| 47 | * | |
| 48 | * @param title The context for the message. | |
| 49 | * @param message The message content (formatted with the given args). | |
| 50 | * @param args Parameters for the message content. | |
| 51 | * @return A notification instance, never null. | |
| 52 | */ | |
| 53 | @Override | |
| 54 | public Notification createNotification( | |
| 55 | final String title, | |
| 56 | final String message, | |
| 57 | final Object... args ) { | |
| 58 | return new DefaultNotification( title, message, args ); | |
| 59 | } | |
| 60 | ||
| 61 | private Alert createAlertDialog( | |
| 62 | final Window parent, | |
| 63 | final AlertType alertType, | |
| 64 | final Notification message ) { | |
| 65 | ||
| 66 | final Alert alert = new Alert( alertType ); | |
| 67 | ||
| 68 | alert.setDialogPane( new ButtonOrderPane() ); | |
| 69 | alert.setTitle( message.getTitle() ); | |
| 70 | alert.setHeaderText( null ); | |
| 71 | alert.setContentText( message.getContent() ); | |
| 72 | alert.initOwner( parent ); | |
| 73 | ||
| 74 | return alert; | |
| 75 | } | |
| 76 | ||
| 77 | @Override | |
| 78 | public Alert createConfirmation( final Window parent, | |
| 79 | final Notification message ) { | |
| 80 | final Alert alert = createAlertDialog( parent, CONFIRMATION, message ); | |
| 81 | ||
| 82 | alert.getButtonTypes().setAll( YES, NO, CANCEL ); | |
| 83 | ||
| 84 | return alert; | |
| 85 | } | |
| 86 | ||
| 87 | @Override | |
| 88 | public Alert createError( final Window parent, final Notification message ) { | |
| 89 | return createAlertDialog( parent, ERROR, message ); | |
| 90 | } | |
| 91 | } | |
| 92 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.scrivenvar.service.impl; | |
| 28 | ||
| 29 | import com.scrivenvar.service.Options; | |
| 30 | ||
| 31 | import java.util.prefs.BackingStoreException; | |
| 32 | import java.util.prefs.Preferences; | |
| 33 | ||
| 34 | import static com.scrivenvar.Constants.PREFS_ROOT; | |
| 35 | import static com.scrivenvar.Constants.PREFS_STATE; | |
| 36 | import static java.util.prefs.Preferences.userRoot; | |
| 37 | ||
| 38 | /** | |
| 39 | * Persistent options user can change at runtime. | |
| 40 | */ | |
| 41 | public class DefaultOptions implements Options { | |
| 42 | public DefaultOptions() { | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * This will throw IllegalArgumentException if the value exceeds the maximum | |
| 47 | * preferences value length. | |
| 48 | * | |
| 49 | * @param key The name of the key to associate with the value. | |
| 50 | * @param value The value to persist. | |
| 51 | * @throws BackingStoreException New value not persisted. | |
| 52 | */ | |
| 53 | @Override | |
| 54 | public void put( final String key, final String value ) | |
| 55 | throws BackingStoreException { | |
| 56 | getState().put( key, value ); | |
| 57 | getState().flush(); | |
| 58 | } | |
| 59 | ||
| 60 | @Override | |
| 61 | public String get( final String key, final String value ) { | |
| 62 | return getState().get( key, value ); | |
| 63 | } | |
| 64 | ||
| 65 | @Override | |
| 66 | public String get( final String key ) { | |
| 67 | return get( key, "" ); | |
| 68 | } | |
| 69 | ||
| 70 | private Preferences getRootPreferences() { | |
| 71 | return userRoot().node( PREFS_ROOT ); | |
| 72 | } | |
| 73 | ||
| 74 | @Override | |
| 75 | public Preferences getState() { | |
| 76 | return getRootPreferences().node( PREFS_STATE ); | |
| 77 | } | |
| 78 | } | |
| 79 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service.impl; | |
| 29 | ||
| 30 | import com.scrivenvar.service.Settings; | |
| 31 | import org.apache.commons.configuration2.PropertiesConfiguration; | |
| 32 | import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler; | |
| 33 | import org.apache.commons.configuration2.convert.ListDelimiterHandler; | |
| 34 | import org.apache.commons.configuration2.ex.ConfigurationException; | |
| 35 | ||
| 36 | import java.io.IOException; | |
| 37 | import java.io.InputStreamReader; | |
| 38 | import java.io.Reader; | |
| 39 | import java.net.URL; | |
| 40 | import java.nio.charset.Charset; | |
| 41 | import java.util.Iterator; | |
| 42 | import java.util.List; | |
| 43 | ||
| 44 | import static com.scrivenvar.Constants.SETTINGS_NAME; | |
| 45 | ||
| 46 | /** | |
| 47 | * Responsible for loading settings that help avoid hard-coded assumptions. | |
| 48 | */ | |
| 49 | public class DefaultSettings implements Settings { | |
| 50 | ||
| 51 | private static final char VALUE_SEPARATOR = ','; | |
| 52 | ||
| 53 | private PropertiesConfiguration properties; | |
| 54 | ||
| 55 | public DefaultSettings() throws ConfigurationException { | |
| 56 | setProperties( createProperties() ); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Returns the value of a string property. | |
| 61 | * | |
| 62 | * @param property The property key. | |
| 63 | * @param defaultValue The value to return if no property key has been set. | |
| 64 | * @return The property key value, or defaultValue when no key found. | |
| 65 | */ | |
| 66 | @Override | |
| 67 | public String getSetting( final String property, final String defaultValue ) { | |
| 68 | return getSettings().getString( property, defaultValue ); | |
| 69 | } | |
| 70 | ||
| 71 | /** | |
| 72 | * Returns the value of a string property. | |
| 73 | * | |
| 74 | * @param property The property key. | |
| 75 | * @param defaultValue The value to return if no property key has been set. | |
| 76 | * @return The property key value, or defaultValue when no key found. | |
| 77 | */ | |
| 78 | @Override | |
| 79 | public int getSetting( final String property, final int defaultValue ) { | |
| 80 | return getSettings().getInt( property, defaultValue ); | |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Convert the generic list of property objects into strings. | |
| 85 | * | |
| 86 | * @param property The property value to coerce. | |
| 87 | * @param defaults The defaults values to use should the property be unset. | |
| 88 | * @return The list of properties coerced from objects to strings. | |
| 89 | */ | |
| 90 | @Override | |
| 91 | public List<String> getStringSettingList( | |
| 92 | final String property, final List<String> defaults ) { | |
| 93 | return getSettings().getList( String.class, property, defaults ); | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Convert a list of property objects into strings, with no default value. | |
| 98 | * | |
| 99 | * @param property The property value to coerce. | |
| 100 | * @return The list of properties coerced from objects to strings. | |
| 101 | */ | |
| 102 | @Override | |
| 103 | public List<String> getStringSettingList( final String property ) { | |
| 104 | return getStringSettingList( property, null ); | |
| 105 | } | |
| 106 | ||
| 107 | /** | |
| 108 | * Returns a list of property names that begin with the given prefix. | |
| 109 | * | |
| 110 | * @param prefix The prefix to compare against each property name. | |
| 111 | * @return The list of property names that have the given prefix. | |
| 112 | */ | |
| 113 | @Override | |
| 114 | public Iterator<String> getKeys( final String prefix ) { | |
| 115 | return getSettings().getKeys( prefix ); | |
| 116 | } | |
| 117 | ||
| 118 | private PropertiesConfiguration createProperties() | |
| 119 | throws ConfigurationException { | |
| 120 | ||
| 121 | final URL url = getPropertySource(); | |
| 122 | final PropertiesConfiguration configuration = new PropertiesConfiguration(); | |
| 123 | ||
| 124 | if( url != null ) { | |
| 125 | try( final Reader r = new InputStreamReader( url.openStream(), | |
| 126 | getDefaultEncoding() ) ) { | |
| 127 | configuration.setListDelimiterHandler( createListDelimiterHandler() ); | |
| 128 | configuration.read( r ); | |
| 129 | ||
| 130 | } catch( final IOException ex ) { | |
| 131 | throw new RuntimeException( new ConfigurationException( ex ) ); | |
| 132 | } | |
| 133 | } | |
| 134 | ||
| 135 | return configuration; | |
| 136 | } | |
| 137 | ||
| 138 | protected Charset getDefaultEncoding() { | |
| 139 | return Charset.defaultCharset(); | |
| 140 | } | |
| 141 | ||
| 142 | protected ListDelimiterHandler createListDelimiterHandler() { | |
| 143 | return new DefaultListDelimiterHandler( VALUE_SEPARATOR ); | |
| 144 | } | |
| 145 | ||
| 146 | private URL getPropertySource() { | |
| 147 | return DefaultSettings.class.getResource( getSettingsFilename() ); | |
| 148 | } | |
| 149 | ||
| 150 | private String getSettingsFilename() { | |
| 151 | return SETTINGS_NAME; | |
| 152 | } | |
| 153 | ||
| 154 | private void setProperties( final PropertiesConfiguration configuration ) { | |
| 155 | this.properties = configuration; | |
| 156 | } | |
| 157 | ||
| 158 | private PropertiesConfiguration getSettings() { | |
| 159 | return this.properties; | |
| 160 | } | |
| 161 | } | |
| 162 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.service.impl; | |
| 29 | ||
| 30 | import com.scrivenvar.service.Snitch; | |
| 31 | ||
| 32 | import java.io.IOException; | |
| 33 | import java.nio.file.*; | |
| 34 | import java.util.Collections; | |
| 35 | import java.util.Map; | |
| 36 | import java.util.Observable; | |
| 37 | import java.util.Set; | |
| 38 | import java.util.concurrent.ConcurrentHashMap; | |
| 39 | ||
| 40 | import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT; | |
| 41 | import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; | |
| 42 | ||
| 43 | /** | |
| 44 | * Listens for file changes. Other classes can register paths to be monitored | |
| 45 | * and listen for changes to those paths. | |
| 46 | */ | |
| 47 | public class DefaultSnitch extends Observable implements Snitch { | |
| 48 | ||
| 49 | /** | |
| 50 | * Service for listening to directories for modifications. | |
| 51 | */ | |
| 52 | private WatchService watchService; | |
| 53 | ||
| 54 | /** | |
| 55 | * Directories being monitored for changes. | |
| 56 | */ | |
| 57 | private Map<WatchKey, Path> keys; | |
| 58 | ||
| 59 | /** | |
| 60 | * Files that will kick off notification events if modified. | |
| 61 | */ | |
| 62 | private Set<Path> eavesdropped; | |
| 63 | ||
| 64 | /** | |
| 65 | * Set to true when running; set to false to stop listening. | |
| 66 | */ | |
| 67 | private volatile boolean listening; | |
| 68 | ||
| 69 | public DefaultSnitch() { | |
| 70 | } | |
| 71 | ||
| 72 | @Override | |
| 73 | public void stop() { | |
| 74 | setListening( false ); | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Adds a listener to the list of files to watch for changes. If the file is | |
| 79 | * already in the monitored list, this will return immediately. | |
| 80 | * | |
| 81 | * @param file Path to a file to watch for changes. | |
| 82 | * @throws IOException The file could not be monitored. | |
| 83 | */ | |
| 84 | @Override | |
| 85 | public void listen( final Path file ) throws IOException { | |
| 86 | if( file != null && getEavesdropped().add( file ) ) { | |
| 87 | final Path dir = toDirectory( file ); | |
| 88 | final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY ); | |
| 89 | ||
| 90 | getWatchMap().put( key, dir ); | |
| 91 | } | |
| 92 | } | |
| 93 | ||
| 94 | /** | |
| 95 | * Returns the given path to a file (or directory) as a directory. If the | |
| 96 | * given path is already a directory, it is returned. Otherwise, this returns | |
| 97 | * the directory that contains the file. This will fail if the file is stored | |
| 98 | * in the root folder. | |
| 99 | * | |
| 100 | * @param path The file to return as a directory, which should always be the | |
| 101 | * case. | |
| 102 | * @return The given path as a directory, if a file, otherwise the path | |
| 103 | * itself. | |
| 104 | */ | |
| 105 | private Path toDirectory( final Path path ) { | |
| 106 | return Files.isDirectory( path ) | |
| 107 | ? path | |
| 108 | : path.toFile().getParentFile().toPath(); | |
| 109 | } | |
| 110 | ||
| 111 | /** | |
| 112 | * Stop listening to the given file for change events. This fails silently. | |
| 113 | * | |
| 114 | * @param file The file to no longer monitor for changes. | |
| 115 | */ | |
| 116 | @Override | |
| 117 | public void ignore( final Path file ) { | |
| 118 | if( file != null ) { | |
| 119 | final Path directory = toDirectory( file ); | |
| 120 | ||
| 121 | // Remove all occurrences (there should be only one). | |
| 122 | getWatchMap().values().removeAll( Collections.singleton( directory ) ); | |
| 123 | ||
| 124 | // Remove all occurrences (there can be only one). | |
| 125 | getEavesdropped().remove( file ); | |
| 126 | } | |
| 127 | } | |
| 128 | ||
| 129 | /** | |
| 130 | * Loops until stop is called, or the application is terminated. | |
| 131 | */ | |
| 132 | @Override | |
| 133 | @SuppressWarnings("BusyWait") | |
| 134 | public void run() { | |
| 135 | setListening( true ); | |
| 136 | ||
| 137 | while( isListening() ) { | |
| 138 | try { | |
| 139 | final WatchKey key = getWatchService().take(); | |
| 140 | final Path path = get( key ); | |
| 141 | ||
| 142 | // Prevent receiving two separate ENTRY_MODIFY events: file modified | |
| 143 | // and timestamp updated. Instead, receive one ENTRY_MODIFY event | |
| 144 | // with two counts. | |
| 145 | Thread.sleep( APP_WATCHDOG_TIMEOUT ); | |
| 146 | ||
| 147 | for( final WatchEvent<?> event : key.pollEvents() ) { | |
| 148 | final Path changed = path.resolve( (Path) event.context() ); | |
| 149 | ||
| 150 | if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) { | |
| 151 | setChanged(); | |
| 152 | notifyObservers( changed ); | |
| 153 | } | |
| 154 | } | |
| 155 | ||
| 156 | if( !key.reset() ) { | |
| 157 | ignore( path ); | |
| 158 | } | |
| 159 | } catch( final IOException | InterruptedException ex ) { | |
| 160 | // Stop eavesdropping. | |
| 161 | setListening( false ); | |
| 162 | } | |
| 163 | } | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Returns true if the list of files being listened to for changes contains | |
| 168 | * the given file. | |
| 169 | * | |
| 170 | * @param file Path to a system file. | |
| 171 | * @return true The given file is being monitored for changes. | |
| 172 | */ | |
| 173 | private boolean isListening( final Path file ) { | |
| 174 | return getEavesdropped().contains( file ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Returns a path for a given watch key. | |
| 179 | * | |
| 180 | * @param key The key to lookup its corresponding path. | |
| 181 | * @return The path for the given key. | |
| 182 | */ | |
| 183 | private Path get( final WatchKey key ) { | |
| 184 | return getWatchMap().get( key ); | |
| 185 | } | |
| 186 | ||
| 187 | private synchronized Map<WatchKey, Path> getWatchMap() { | |
| 188 | if( this.keys == null ) { | |
| 189 | this.keys = createWatchKeys(); | |
| 190 | } | |
| 191 | ||
| 192 | return this.keys; | |
| 193 | } | |
| 194 | ||
| 195 | protected Map<WatchKey, Path> createWatchKeys() { | |
| 196 | return new ConcurrentHashMap<>(); | |
| 197 | } | |
| 198 | ||
| 199 | /** | |
| 200 | * Returns a list of files that, when changed, will kick off a notification. | |
| 201 | * | |
| 202 | * @return A non-null, possibly empty, list of files. | |
| 203 | */ | |
| 204 | private synchronized Set<Path> getEavesdropped() { | |
| 205 | if( this.eavesdropped == null ) { | |
| 206 | this.eavesdropped = createEavesdropped(); | |
| 207 | } | |
| 208 | ||
| 209 | return this.eavesdropped; | |
| 210 | } | |
| 211 | ||
| 212 | protected Set<Path> createEavesdropped() { | |
| 213 | return ConcurrentHashMap.newKeySet(); | |
| 214 | } | |
| 215 | ||
| 216 | /** | |
| 217 | * The existing watch service, or a new instance if null. | |
| 218 | * | |
| 219 | * @return A valid WatchService instance, never null. | |
| 220 | * @throws IOException Could not create a new watch service. | |
| 221 | */ | |
| 222 | private synchronized WatchService getWatchService() throws IOException { | |
| 223 | if( this.watchService == null ) { | |
| 224 | this.watchService = createWatchService(); | |
| 225 | } | |
| 226 | ||
| 227 | return this.watchService; | |
| 228 | } | |
| 229 | ||
| 230 | protected WatchService createWatchService() throws IOException { | |
| 231 | final FileSystem fileSystem = FileSystems.getDefault(); | |
| 232 | return fileSystem.newWatchService(); | |
| 233 | } | |
| 234 | ||
| 235 | /** | |
| 236 | * Answers whether the loop should continue executing. | |
| 237 | * | |
| 238 | * @return true The internal listening loop should continue listening for file | |
| 239 | * modification events. | |
| 240 | */ | |
| 241 | protected boolean isListening() { | |
| 242 | return this.listening; | |
| 243 | } | |
| 244 | ||
| 245 | /** | |
| 246 | * Requests the snitch to stop eavesdropping on file changes. | |
| 247 | * | |
| 248 | * @param listening Use true to indicate the service should stop running. | |
| 249 | */ | |
| 250 | private void setListening( final boolean listening ) { | |
| 251 | this.listening = listening; | |
| 252 | } | |
| 253 | } | |
| 254 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.sigils; | |
| 29 | ||
| 30 | import static com.scrivenvar.sigils.YamlSigilOperator.KEY_SEPARATOR_DEF; | |
| 31 | ||
| 32 | /** | |
| 33 | * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils. | |
| 34 | */ | |
| 35 | public class RSigilOperator extends SigilOperator { | |
| 36 | public static final char KEY_SEPARATOR_R = '$'; | |
| 37 | ||
| 38 | public static final String PREFIX = "`r#"; | |
| 39 | public static final char SUFFIX = '`'; | |
| 40 | ||
| 41 | private final String mDelimiterBegan = | |
| 42 | getUserPreferences().getRDelimiterBegan(); | |
| 43 | private final String mDelimiterEnded = | |
| 44 | getUserPreferences().getRDelimiterEnded(); | |
| 45 | ||
| 46 | /** | |
| 47 | * Returns the given string R-escaping backticks prepended and appended. This | |
| 48 | * is not null safe. Do not pass null into this method. | |
| 49 | * | |
| 50 | * @param key The string to adorn with R token delimiters. | |
| 51 | * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`". | |
| 52 | */ | |
| 53 | @Override | |
| 54 | public String apply( final String key ) { | |
| 55 | assert key != null; | |
| 56 | ||
| 57 | return PREFIX | |
| 58 | + mDelimiterBegan | |
| 59 | + entoken( key ) | |
| 60 | + mDelimiterEnded | |
| 61 | + SUFFIX; | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Transforms a definition key (bracketed by token delimiters) into the | |
| 66 | * expected format for an R variable key name. | |
| 67 | * | |
| 68 | * @param key The variable name to transform, can be empty but not null. | |
| 69 | * @return The transformed variable name. | |
| 70 | */ | |
| 71 | public static String entoken( final String key ) { | |
| 72 | return "v$" + | |
| 73 | YamlSigilOperator.detoken( key ) | |
| 74 | .replace( KEY_SEPARATOR_DEF, KEY_SEPARATOR_R ); | |
| 75 | } | |
| 76 | } | |
| 77 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.sigils; | |
| 29 | ||
| 30 | import com.scrivenvar.preferences.UserPreferences; | |
| 31 | ||
| 32 | import java.util.function.UnaryOperator; | |
| 33 | ||
| 34 | /** | |
| 35 | * Responsible for updating definition keys to use a machine-readable format | |
| 36 | * corresponding to the type of file being edited. This changes a definition | |
| 37 | * key name based on some criteria determined by the factory that creates | |
| 38 | * implementations of this interface. | |
| 39 | */ | |
| 40 | public abstract class SigilOperator implements UnaryOperator<String> { | |
| 41 | protected static UserPreferences getUserPreferences() { | |
| 42 | return UserPreferences.getInstance(); | |
| 43 | } | |
| 44 | } | |
| 45 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.sigils; | |
| 29 | ||
| 30 | import java.util.regex.Pattern; | |
| 31 | ||
| 32 | import static java.lang.String.format; | |
| 33 | import static java.util.regex.Pattern.compile; | |
| 34 | import static java.util.regex.Pattern.quote; | |
| 35 | ||
| 36 | /** | |
| 37 | * Brackets definition keys with token delimiters. | |
| 38 | */ | |
| 39 | public class YamlSigilOperator extends SigilOperator { | |
| 40 | public static final char KEY_SEPARATOR_DEF = '.'; | |
| 41 | ||
| 42 | private static final String mDelimiterBegan = | |
| 43 | getUserPreferences().getDefDelimiterBegan(); | |
| 44 | private static final String mDelimiterEnded = | |
| 45 | getUserPreferences().getDefDelimiterEnded(); | |
| 46 | ||
| 47 | /** | |
| 48 | * Non-greedy match of key names delimited by definition tokens. | |
| 49 | */ | |
| 50 | private static final String REGEX = | |
| 51 | format( "(%s.*?%s)", quote( mDelimiterBegan ), quote( mDelimiterEnded ) ); | |
| 52 | ||
| 53 | /** | |
| 54 | * Compiled regular expression for matching delimited references. | |
| 55 | */ | |
| 56 | public static final Pattern REGEX_PATTERN = compile( REGEX ); | |
| 57 | ||
| 58 | /** | |
| 59 | * Returns the given {@link String} verbatim because variables in YAML | |
| 60 | * documents and plain Markdown documents already have the appropriate | |
| 61 | * tokenizable syntax wrapped around the text. | |
| 62 | * | |
| 63 | * @param key Returned verbatim. | |
| 64 | */ | |
| 65 | @Override | |
| 66 | public String apply( final String key ) { | |
| 67 | return key; | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Adds delimiters to the given key. | |
| 72 | * | |
| 73 | * @param key The key to adorn with start and stop definition tokens. | |
| 74 | * @return The given key bracketed by definition token symbols. | |
| 75 | */ | |
| 76 | public static String entoken( final String key ) { | |
| 77 | assert key != null; | |
| 78 | return mDelimiterBegan + key + mDelimiterEnded; | |
| 79 | } | |
| 80 | ||
| 81 | /** | |
| 82 | * Removes start and stop definition key delimiters from the given key. This | |
| 83 | * method does not check for delimiters, only that there are sufficient | |
| 84 | * characters to remove from either end of the given key. | |
| 85 | * | |
| 86 | * @param key The key adorned with start and stop definition tokens. | |
| 87 | * @return The given key with the delimiters removed. | |
| 88 | */ | |
| 89 | public static String detoken( final String key ) { | |
| 90 | final int beganLen = mDelimiterBegan.length(); | |
| 91 | final int endedLen = mDelimiterEnded.length(); | |
| 92 | ||
| 93 | return key.length() > beganLen + endedLen | |
| 94 | ? key.substring( beganLen, key.length() - endedLen ) | |
| 95 | : key; | |
| 96 | } | |
| 97 | } | |
| 98 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.spelling.api; | |
| 29 | ||
| 30 | import java.util.function.BiConsumer; | |
| 31 | ||
| 32 | /** | |
| 33 | * Represents an operation that accepts two input arguments and returns no | |
| 34 | * result. Unlike most other functional interfaces, this class is expected to | |
| 35 | * operate via side-effects. | |
| 36 | * <p> | |
| 37 | * This is used instead of a {@link BiConsumer} to avoid autoboxing. | |
| 38 | * </p> | |
| 39 | */ | |
| 40 | @FunctionalInterface | |
| 41 | public interface SpellCheckListener { | |
| 42 | ||
| 43 | /** | |
| 44 | * Performs an operation on the given arguments. | |
| 45 | * | |
| 46 | * @param text The text associated with a beginning and ending offset. | |
| 47 | * @param beganOffset A starting offset, used as an index into a string. | |
| 48 | * @param endedOffset An ending offset, which should equal text.length() + | |
| 49 | * beganOffset. | |
| 50 | */ | |
| 51 | void accept( String text, int beganOffset, int endedOffset ); | |
| 52 | } | |
| 53 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.spelling.api; | |
| 29 | ||
| 30 | import java.util.List; | |
| 31 | ||
| 32 | /** | |
| 33 | * Defines the responsibilities for a spell checking API. The intention is | |
| 34 | * to allow different spell checking implementations to be used by the | |
| 35 | * application, such as SymSpell and LinSpell. | |
| 36 | */ | |
| 37 | public interface SpellChecker { | |
| 38 | ||
| 39 | /** | |
| 40 | * Answers whether the given lexeme, in whole, is found in the lexicon. The | |
| 41 | * lexicon lookup is performed case-insensitively. This method should be | |
| 42 | * used instead of {@link #suggestions(String, int)} for performance reasons. | |
| 43 | * | |
| 44 | * @param lexeme The word to check for correctness. | |
| 45 | * @return {@code true} if the lexeme is in the lexicon. | |
| 46 | */ | |
| 47 | boolean inLexicon( String lexeme ); | |
| 48 | ||
| 49 | /** | |
| 50 | * Gets a list of spelling corrections for the given lexeme. | |
| 51 | * | |
| 52 | * @param lexeme A word to check for correctness that's not in the lexicon. | |
| 53 | * @param count The maximum number of alternatives to return. | |
| 54 | * @return A list of words in the lexicon that are similar to the given | |
| 55 | * lexeme. | |
| 56 | */ | |
| 57 | List<String> suggestions( String lexeme, int count ); | |
| 58 | ||
| 59 | /** | |
| 60 | * Iterates over the given text, emitting starting and ending offsets into | |
| 61 | * the text for every word that is missing from the lexicon. | |
| 62 | * | |
| 63 | * @param text The text to check for words missing from the lexicon. | |
| 64 | * @param consumer Every missing word emits a message with the starting | |
| 65 | * and ending offset into the text where said word is found. | |
| 66 | */ | |
| 67 | void proofread( String text, SpellCheckListener consumer ); | |
| 68 | } | |
| 69 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.spelling.impl; | |
| 29 | ||
| 30 | import com.scrivenvar.spelling.api.SpellCheckListener; | |
| 31 | import com.scrivenvar.spelling.api.SpellChecker; | |
| 32 | ||
| 33 | import java.util.List; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for spell checking in the event that a real spell checking | |
| 37 | * implementation cannot be created (for any reason). Does not perform any | |
| 38 | * spell checking and indicates that any given lexeme is in the lexicon. | |
| 39 | */ | |
| 40 | public class PermissiveSpeller implements SpellChecker { | |
| 41 | /** | |
| 42 | * Returns {@code true}, ignoring the given word. | |
| 43 | * | |
| 44 | * @param ignored Unused. | |
| 45 | * @return {@code true} | |
| 46 | */ | |
| 47 | @Override | |
| 48 | public boolean inLexicon( final String ignored ) { | |
| 49 | return true; | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Returns an array with the given lexeme. | |
| 54 | * | |
| 55 | * @param lexeme The word to return. | |
| 56 | * @param ignored Unused. | |
| 57 | * @return A suggestion list containing the given lexeme. | |
| 58 | */ | |
| 59 | @Override | |
| 60 | public List<String> suggestions( final String lexeme, final int ignored ) { | |
| 61 | return List.of( lexeme ); | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Performs no action. | |
| 66 | * | |
| 67 | * @param text Unused. | |
| 68 | * @param ignored Uncalled. | |
| 69 | */ | |
| 70 | @Override | |
| 71 | public void proofread( | |
| 72 | final String text, final SpellCheckListener ignored ) { | |
| 73 | } | |
| 74 | } | |
| 75 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.spelling.impl; | |
| 29 | ||
| 30 | import com.scrivenvar.spelling.api.SpellCheckListener; | |
| 31 | import com.scrivenvar.spelling.api.SpellChecker; | |
| 32 | import io.gitlab.rxp90.jsymspell.SuggestItem; | |
| 33 | import io.gitlab.rxp90.jsymspell.SymSpell; | |
| 34 | import io.gitlab.rxp90.jsymspell.SymSpellBuilder; | |
| 35 | ||
| 36 | import java.text.BreakIterator; | |
| 37 | import java.util.ArrayList; | |
| 38 | import java.util.Collection; | |
| 39 | import java.util.List; | |
| 40 | ||
| 41 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity; | |
| 42 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL; | |
| 43 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST; | |
| 44 | import static java.lang.Character.isLetter; | |
| 45 | ||
| 46 | /** | |
| 47 | * Responsible for spell checking using {@link SymSpell}. | |
| 48 | */ | |
| 49 | public class SymSpellSpeller implements SpellChecker { | |
| 50 | private final BreakIterator mBreakIterator = BreakIterator.getWordInstance(); | |
| 51 | ||
| 52 | private final SymSpell mSymSpell; | |
| 53 | ||
| 54 | /** | |
| 55 | * Creates a new lexicon for the given collection of lexemes. | |
| 56 | * | |
| 57 | * @param lexiconWords The words in the lexicon to add for spell checking, | |
| 58 | * must not be empty. | |
| 59 | * @return An instance of {@link SpellChecker} that can check if a word | |
| 60 | * is correct and suggest alternatives. | |
| 61 | */ | |
| 62 | public static SpellChecker forLexicon( | |
| 63 | final Collection<String> lexiconWords ) { | |
| 64 | assert lexiconWords != null && !lexiconWords.isEmpty(); | |
| 65 | ||
| 66 | final SymSpellBuilder builder = new SymSpellBuilder() | |
| 67 | .setLexiconWords( lexiconWords ); | |
| 68 | ||
| 69 | return new SymSpellSpeller( builder.build() ); | |
| 70 | } | |
| 71 | ||
| 72 | /** | |
| 73 | * Prevent direct instantiation so that only the {@link SpellChecker} | |
| 74 | * interface | |
| 75 | * is available. | |
| 76 | * | |
| 77 | * @param symSpell The implementation-specific spell checker. | |
| 78 | */ | |
| 79 | private SymSpellSpeller( final SymSpell symSpell ) { | |
| 80 | mSymSpell = symSpell; | |
| 81 | } | |
| 82 | ||
| 83 | @Override | |
| 84 | public boolean inLexicon( final String lexeme ) { | |
| 85 | return lookup( lexeme, CLOSEST ).size() == 1; | |
| 86 | } | |
| 87 | ||
| 88 | @Override | |
| 89 | public List<String> suggestions( final String lexeme, int count ) { | |
| 90 | final List<String> result = new ArrayList<>( count ); | |
| 91 | ||
| 92 | for( final var item : lookup( lexeme, ALL ) ) { | |
| 93 | if( count-- > 0 ) { | |
| 94 | result.add( item.getSuggestion() ); | |
| 95 | } | |
| 96 | else { | |
| 97 | break; | |
| 98 | } | |
| 99 | } | |
| 100 | ||
| 101 | return result; | |
| 102 | } | |
| 103 | ||
| 104 | @Override | |
| 105 | public void proofread( | |
| 106 | final String text, final SpellCheckListener consumer ) { | |
| 107 | assert text != null; | |
| 108 | assert consumer != null; | |
| 109 | ||
| 110 | mBreakIterator.setText( text ); | |
| 111 | ||
| 112 | int boundaryIndex = mBreakIterator.first(); | |
| 113 | int previousIndex = 0; | |
| 114 | ||
| 115 | while( boundaryIndex != BreakIterator.DONE ) { | |
| 116 | final var lex = text.substring( previousIndex, boundaryIndex ) | |
| 117 | .toLowerCase(); | |
| 118 | ||
| 119 | // Get the lexeme for the possessive. | |
| 120 | final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" ); | |
| 121 | final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex; | |
| 122 | ||
| 123 | if( isWord( lexeme ) && !inLexicon( lexeme ) ) { | |
| 124 | consumer.accept( lex, previousIndex, boundaryIndex ); | |
| 125 | } | |
| 126 | ||
| 127 | previousIndex = boundaryIndex; | |
| 128 | boundaryIndex = mBreakIterator.next(); | |
| 129 | } | |
| 130 | } | |
| 131 | ||
| 132 | /** | |
| 133 | * Answers whether the given string is likely a word by checking the first | |
| 134 | * character. | |
| 135 | * | |
| 136 | * @param word The word to check. | |
| 137 | * @return {@code true} if the word begins with a letter. | |
| 138 | */ | |
| 139 | private boolean isWord( final String word ) { | |
| 140 | return !word.isEmpty() && isLetter( word.charAt( 0 ) ); | |
| 141 | } | |
| 142 | ||
| 143 | /** | |
| 144 | * Returns a list of {@link SuggestItem} instances that provide alternative | |
| 145 | * spellings for the given lexeme. | |
| 146 | * | |
| 147 | * @param lexeme A word to look up in the lexicon. | |
| 148 | * @param v Influences the number of results returned. | |
| 149 | * @return Alternative lexemes. | |
| 150 | */ | |
| 151 | private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) { | |
| 152 | return getSpeller().lookup( lexeme, v ); | |
| 153 | } | |
| 154 | ||
| 155 | private SymSpell getSpeller() { | |
| 156 | return mSymSpell; | |
| 157 | } | |
| 158 | } | |
| 159 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.scrivenvar.util; | |
| 28 | ||
| 29 | import de.jensd.fx.glyphs.GlyphIcons; | |
| 30 | import javafx.beans.value.ObservableBooleanValue; | |
| 31 | import javafx.event.ActionEvent; | |
| 32 | import javafx.event.EventHandler; | |
| 33 | import javafx.scene.input.KeyCombination; | |
| 34 | ||
| 35 | /** | |
| 36 | * Defines actions the user can take by interacting with the GUI. | |
| 37 | */ | |
| 38 | public class Action { | |
| 39 | public final String text; | |
| 40 | public final KeyCombination accelerator; | |
| 41 | public final GlyphIcons icon; | |
| 42 | public final EventHandler<ActionEvent> action; | |
| 43 | public final ObservableBooleanValue disable; | |
| 44 | ||
| 45 | public Action( | |
| 46 | final String text, | |
| 47 | final String accelerator, | |
| 48 | final GlyphIcons icon, | |
| 49 | final EventHandler<ActionEvent> action, | |
| 50 | final ObservableBooleanValue disable ) { | |
| 51 | ||
| 52 | this.text = text; | |
| 53 | this.accelerator = accelerator == null ? | |
| 54 | null : KeyCombination.valueOf( accelerator ); | |
| 55 | this.icon = icon; | |
| 56 | this.action = action; | |
| 57 | this.disable = disable; | |
| 58 | } | |
| 59 | } | |
| 60 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.util; | |
| 29 | ||
| 30 | import com.scrivenvar.Messages; | |
| 31 | import de.jensd.fx.glyphs.GlyphIcons; | |
| 32 | import javafx.beans.value.ObservableBooleanValue; | |
| 33 | import javafx.event.ActionEvent; | |
| 34 | import javafx.event.EventHandler; | |
| 35 | ||
| 36 | /** | |
| 37 | * Provides a fluent interface around constructing actions so that duplication | |
| 38 | * can be avoided. | |
| 39 | */ | |
| 40 | public class ActionBuilder { | |
| 41 | private String mText; | |
| 42 | private String mAccelerator; | |
| 43 | private GlyphIcons mIcon; | |
| 44 | private EventHandler<ActionEvent> mAction; | |
| 45 | private ObservableBooleanValue mDisable; | |
| 46 | ||
| 47 | /** | |
| 48 | * Sets the action text based on a resource bundle key. | |
| 49 | * | |
| 50 | * @param key The key to look up in the {@link Messages}. | |
| 51 | * @return The corresponding value, or the key name if none found. | |
| 52 | */ | |
| 53 | public ActionBuilder setText( final String key ) { | |
| 54 | mText = Messages.get( key, key ); | |
| 55 | return this; | |
| 56 | } | |
| 57 | ||
| 58 | public ActionBuilder setAccelerator( final String accelerator ) { | |
| 59 | mAccelerator = accelerator; | |
| 60 | return this; | |
| 61 | } | |
| 62 | ||
| 63 | public ActionBuilder setIcon( final GlyphIcons icon ) { | |
| 64 | mIcon = icon; | |
| 65 | return this; | |
| 66 | } | |
| 67 | ||
| 68 | public ActionBuilder setAction( final EventHandler<ActionEvent> action ) { | |
| 69 | mAction = action; | |
| 70 | return this; | |
| 71 | } | |
| 72 | ||
| 73 | public ActionBuilder setDisable( final ObservableBooleanValue disable ) { | |
| 74 | mDisable = disable; | |
| 75 | return this; | |
| 76 | } | |
| 77 | ||
| 78 | public Action build() { | |
| 79 | return new Action( mText, mAccelerator, mIcon, mAction, mDisable ); | |
| 80 | } | |
| 81 | } | |
| 82 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.scrivenvar.util; | |
| 28 | ||
| 29 | import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory; | |
| 30 | import javafx.scene.Node; | |
| 31 | import javafx.scene.control.Button; | |
| 32 | import javafx.scene.control.Menu; | |
| 33 | import javafx.scene.control.MenuItem; | |
| 34 | import javafx.scene.control.Separator; | |
| 35 | import javafx.scene.control.SeparatorMenuItem; | |
| 36 | import javafx.scene.control.ToolBar; | |
| 37 | import javafx.scene.control.Tooltip; | |
| 38 | ||
| 39 | /** | |
| 40 | * Responsible for creating menu items and toolbar buttons. | |
| 41 | */ | |
| 42 | public class ActionUtils { | |
| 43 | ||
| 44 | public static Menu createMenu( final String text, final Action... actions ) { | |
| 45 | return new Menu( text, null, createMenuItems( actions ) ); | |
| 46 | } | |
| 47 | ||
| 48 | public static MenuItem[] createMenuItems( final Action... actions ) { | |
| 49 | final MenuItem[] menuItems = new MenuItem[ actions.length ]; | |
| 50 | ||
| 51 | for( int i = 0; i < actions.length; i++ ) { | |
| 52 | menuItems[ i ] = (actions[ i ] == null) | |
| 53 | ? new SeparatorMenuItem() | |
| 54 | : createMenuItem( actions[ i ] ); | |
| 55 | } | |
| 56 | ||
| 57 | return menuItems; | |
| 58 | } | |
| 59 | ||
| 60 | public static MenuItem createMenuItem( final Action action ) { | |
| 61 | final MenuItem menuItem = new MenuItem( action.text ); | |
| 62 | ||
| 63 | if( action.accelerator != null ) { | |
| 64 | menuItem.setAccelerator( action.accelerator ); | |
| 65 | } | |
| 66 | ||
| 67 | if( action.icon != null ) { | |
| 68 | menuItem.setGraphic( | |
| 69 | FontAwesomeIconFactory.get().createIcon( action.icon ) ); | |
| 70 | } | |
| 71 | ||
| 72 | menuItem.setOnAction( action.action ); | |
| 73 | ||
| 74 | if( action.disable != null ) { | |
| 75 | menuItem.disableProperty().bind( action.disable ); | |
| 76 | } | |
| 77 | ||
| 78 | menuItem.setMnemonicParsing( true ); | |
| 79 | ||
| 80 | return menuItem; | |
| 81 | } | |
| 82 | ||
| 83 | public static ToolBar createToolBar( final Action... actions ) { | |
| 84 | return new ToolBar( createToolBarButtons( actions ) ); | |
| 85 | } | |
| 86 | ||
| 87 | public static Node[] createToolBarButtons( final Action... actions ) { | |
| 88 | Node[] buttons = new Node[ actions.length ]; | |
| 89 | for( int i = 0; i < actions.length; i++ ) { | |
| 90 | buttons[ i ] = (actions[ i ] != null) | |
| 91 | ? createToolBarButton( actions[ i ] ) | |
| 92 | : new Separator(); | |
| 93 | } | |
| 94 | return buttons; | |
| 95 | } | |
| 96 | ||
| 97 | public static Button createToolBarButton( final Action action ) { | |
| 98 | final Button button = new Button(); | |
| 99 | button.setGraphic( | |
| 100 | FontAwesomeIconFactory | |
| 101 | .get() | |
| 102 | .createIcon( action.icon, "1.2em" ) ); | |
| 103 | ||
| 104 | String tooltip = action.text; | |
| 105 | ||
| 106 | if( tooltip.endsWith( "..." ) ) { | |
| 107 | tooltip = tooltip.substring( 0, tooltip.length() - 3 ); | |
| 108 | } | |
| 109 | ||
| 110 | if( action.accelerator != null ) { | |
| 111 | tooltip += " (" + action.accelerator.getDisplayText() + ')'; | |
| 112 | } | |
| 113 | ||
| 114 | button.setTooltip( new Tooltip( tooltip ) ); | |
| 115 | button.setFocusTraversable( false ); | |
| 116 | button.setOnAction( action.action ); | |
| 117 | ||
| 118 | if( action.disable != null ) { | |
| 119 | button.disableProperty().bind( action.disable ); | |
| 120 | } | |
| 121 | ||
| 122 | return button; | |
| 123 | } | |
| 124 | } | |
| 125 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.util; | |
| 29 | ||
| 30 | import java.util.LinkedHashMap; | |
| 31 | import java.util.Map; | |
| 32 | ||
| 33 | /** | |
| 34 | * A map that removes the oldest entry once its capacity (cache size) has | |
| 35 | * been reached. | |
| 36 | * | |
| 37 | * @param <K> The type of key mapped to a value. | |
| 38 | * @param <V> The type of value mapped to a key. | |
| 39 | */ | |
| 40 | public class BoundedCache<K, V> extends LinkedHashMap<K, V> { | |
| 41 | private final int mCacheSize; | |
| 42 | ||
| 43 | /** | |
| 44 | * Constructs a new instance having a finite size. | |
| 45 | * | |
| 46 | * @param cacheSize The maximum number of entries. | |
| 47 | */ | |
| 48 | public BoundedCache( final int cacheSize ) { | |
| 49 | mCacheSize = cacheSize; | |
| 50 | } | |
| 51 | ||
| 52 | @Override | |
| 53 | protected boolean removeEldestEntry( | |
| 54 | final Map.Entry<K, V> eldest ) { | |
| 55 | return size() > mCacheSize; | |
| 56 | } | |
| 57 | } | |
| 58 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.util; | |
| 29 | ||
| 30 | import java.io.File; | |
| 31 | import java.net.MalformedURLException; | |
| 32 | import java.net.URI; | |
| 33 | import java.net.URL; | |
| 34 | ||
| 35 | import static com.scrivenvar.util.ProtocolScheme.UNKNOWN; | |
| 36 | ||
| 37 | /** | |
| 38 | * Responsible for determining the protocol of a resource. | |
| 39 | */ | |
| 40 | public class ProtocolResolver { | |
| 41 | /** | |
| 42 | * Returns the protocol for a given URI or filename. | |
| 43 | * | |
| 44 | * @param resource Determine the protocol for this URI or filename. | |
| 45 | * @return The protocol for the given resource. | |
| 46 | */ | |
| 47 | public static ProtocolScheme getProtocol( final String resource ) { | |
| 48 | String protocol; | |
| 49 | ||
| 50 | try { | |
| 51 | final URI uri = new URI( resource ); | |
| 52 | ||
| 53 | if( uri.isAbsolute() ) { | |
| 54 | protocol = uri.getScheme(); | |
| 55 | } | |
| 56 | else { | |
| 57 | final URL url = new URL( resource ); | |
| 58 | protocol = url.getProtocol(); | |
| 59 | } | |
| 60 | } catch( final Exception e ) { | |
| 61 | // Could be HTTP, HTTPS? | |
| 62 | if( resource.startsWith( "//" ) ) { | |
| 63 | throw new IllegalArgumentException( "Relative context: " + resource ); | |
| 64 | } | |
| 65 | else { | |
| 66 | final File file = new File( resource ); | |
| 67 | protocol = getProtocol( file ); | |
| 68 | } | |
| 69 | } | |
| 70 | ||
| 71 | return ProtocolScheme.valueFrom( protocol ); | |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Returns the protocol for a given file. | |
| 76 | * | |
| 77 | * @param file Determine the protocol for this file. | |
| 78 | * @return The protocol for the given file. | |
| 79 | */ | |
| 80 | private static String getProtocol( final File file ) { | |
| 81 | String result; | |
| 82 | ||
| 83 | try { | |
| 84 | result = file.toURI().toURL().getProtocol(); | |
| 85 | } catch( final MalformedURLException ex ) { | |
| 86 | // Value guaranteed to avoid identification as a standard protocol. | |
| 87 | result = UNKNOWN.toString(); | |
| 88 | } | |
| 89 | ||
| 90 | return result; | |
| 91 | } | |
| 92 | } | |
| 93 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.util; | |
| 29 | ||
| 30 | /** | |
| 31 | * Represents the type of data encoding scheme used for a universal resource | |
| 32 | * indicator. | |
| 33 | */ | |
| 34 | public enum ProtocolScheme { | |
| 35 | /** | |
| 36 | * Denotes either HTTP or HTTPS. | |
| 37 | */ | |
| 38 | HTTP, | |
| 39 | /** | |
| 40 | * Denotes a local file. | |
| 41 | */ | |
| 42 | FILE, | |
| 43 | /** | |
| 44 | * Could not determine schema (or is not supported by the application). | |
| 45 | */ | |
| 46 | UNKNOWN; | |
| 47 | ||
| 48 | /** | |
| 49 | * Answers {@code true} if the given protocol is either HTTP or HTTPS. | |
| 50 | * | |
| 51 | * @return {@code true} the protocol is either HTTP or HTTPS. | |
| 52 | */ | |
| 53 | public boolean isHttp() { | |
| 54 | return this == HTTP; | |
| 55 | } | |
| 56 | ||
| 57 | /** | |
| 58 | * Answers {@code true} if the given protocol is for a local file. | |
| 59 | * | |
| 60 | * @return {@code true} the protocol is for a local file reference. | |
| 61 | */ | |
| 62 | public boolean isFile() { | |
| 63 | return this == FILE; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * Determines the protocol scheme for a given string. | |
| 68 | * | |
| 69 | * @param protocol A string representing data encoding protocol scheme. | |
| 70 | * @return {@link #UNKNOWN} if the protocol is unrecognized, otherwise a | |
| 71 | * valid value from this enumeration. | |
| 72 | */ | |
| 73 | public static ProtocolScheme valueFrom( String protocol ) { | |
| 74 | ProtocolScheme result = UNKNOWN; | |
| 75 | protocol = sanitize( protocol ); | |
| 76 | ||
| 77 | for( final var scheme : values() ) { | |
| 78 | // This will match HTTP/HTTPS as well as FILE*, which may be inaccurate. | |
| 79 | if( protocol.startsWith( scheme.name() ) ) { | |
| 80 | result = scheme; | |
| 81 | break; | |
| 82 | } | |
| 83 | } | |
| 84 | ||
| 85 | return result; | |
| 86 | } | |
| 87 | ||
| 88 | /** | |
| 89 | * Returns an empty string if the given string to sanitize is {@code null}, | |
| 90 | * otherwise the given string in uppercase. Uppercase is used to align with | |
| 91 | * the enum name. | |
| 92 | * | |
| 93 | * @param s The string to sanitize, may be {@code null}. | |
| 94 | * @return A non-{@code null} string. | |
| 95 | */ | |
| 96 | private static String sanitize( final String s ) { | |
| 97 | return s == null ? "" : s.toUpperCase(); | |
| 98 | } | |
| 99 | } | |
| 100 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.util; | |
| 29 | ||
| 30 | import java.io.IOException; | |
| 31 | import java.net.URISyntaxException; | |
| 32 | import java.nio.file.*; | |
| 33 | import java.util.function.Consumer; | |
| 34 | ||
| 35 | import static java.nio.file.FileSystems.newFileSystem; | |
| 36 | import static java.util.Collections.emptyMap; | |
| 37 | ||
| 38 | /** | |
| 39 | * Responsible for finding file resources. | |
| 40 | */ | |
| 41 | public class ResourceWalker { | |
| 42 | private static final PathMatcher PATH_MATCHER = | |
| 43 | FileSystems.getDefault().getPathMatcher( "glob:**.{ttf,otf}" ); | |
| 44 | ||
| 45 | /** | |
| 46 | * @param dirName The root directory to scan for files matching the glob. | |
| 47 | * @param c The consumer function to call for each matching path found. | |
| 48 | * @throws URISyntaxException Could not convert the resource to a URI. | |
| 49 | * @throws IOException Could not walk the tree. | |
| 50 | */ | |
| 51 | public static void walk( final String dirName, final Consumer<Path> c ) | |
| 52 | throws URISyntaxException, IOException { | |
| 53 | final var resource = ResourceWalker.class.getResource( dirName ); | |
| 54 | ||
| 55 | if( resource != null ) { | |
| 56 | final var uri = resource.toURI(); | |
| 57 | final var path = uri.getScheme().equals( "jar" ) | |
| 58 | ? newFileSystem( uri, emptyMap() ).getPath( dirName ) | |
| 59 | : Paths.get( uri ); | |
| 60 | final var walk = Files.walk( path, 10 ); | |
| 61 | ||
| 62 | for( final var it = walk.iterator(); it.hasNext(); ) { | |
| 63 | final Path p = it.next(); | |
| 64 | if( PATH_MATCHER.matches( p ) ) { | |
| 65 | c.accept( p ); | |
| 66 | } | |
| 67 | } | |
| 68 | } | |
| 69 | } | |
| 70 | } | |
| 71 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.scrivenvar.util; | |
| 28 | ||
| 29 | import java.util.prefs.Preferences; | |
| 30 | ||
| 31 | import javafx.application.Platform; | |
| 32 | import javafx.scene.shape.Rectangle; | |
| 33 | import javafx.stage.Stage; | |
| 34 | import javafx.stage.WindowEvent; | |
| 35 | ||
| 36 | /** | |
| 37 | * Saves and restores Stage state (window bounds, maximized, fullScreen). | |
| 38 | */ | |
| 39 | public class StageState { | |
| 40 | ||
| 41 | public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition"; | |
| 42 | public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor"; | |
| 43 | public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview"; | |
| 44 | ||
| 45 | private final Stage mStage; | |
| 46 | private final Preferences mState; | |
| 47 | ||
| 48 | private Rectangle normalBounds; | |
| 49 | private boolean runLaterPending; | |
| 50 | ||
| 51 | public StageState( final Stage stage, final Preferences state ) { | |
| 52 | mStage = stage; | |
| 53 | mState = state; | |
| 54 | ||
| 55 | restore(); | |
| 56 | ||
| 57 | stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() ); | |
| 58 | ||
| 59 | stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 60 | stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 61 | stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 62 | stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() ); | |
| 63 | } | |
| 64 | ||
| 65 | private void save() { | |
| 66 | final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds; | |
| 67 | ||
| 68 | if( bounds != null ) { | |
| 69 | mState.putDouble( "windowX", bounds.getX() ); | |
| 70 | mState.putDouble( "windowY", bounds.getY() ); | |
| 71 | mState.putDouble( "windowWidth", bounds.getWidth() ); | |
| 72 | mState.putDouble( "windowHeight", bounds.getHeight() ); | |
| 73 | } | |
| 74 | ||
| 75 | mState.putBoolean( "windowMaximized", mStage.isMaximized() ); | |
| 76 | mState.putBoolean( "windowFullScreen", mStage.isFullScreen() ); | |
| 77 | } | |
| 78 | ||
| 79 | private void restore() { | |
| 80 | final double x = mState.getDouble( "windowX", Double.NaN ); | |
| 81 | final double y = mState.getDouble( "windowY", Double.NaN ); | |
| 82 | final double w = mState.getDouble( "windowWidth", Double.NaN ); | |
| 83 | final double h = mState.getDouble( "windowHeight", Double.NaN ); | |
| 84 | final boolean maximized = mState.getBoolean( "windowMaximized", false ); | |
| 85 | final boolean fullScreen = mState.getBoolean( "windowFullScreen", false ); | |
| 86 | ||
| 87 | if( !Double.isNaN( x ) && !Double.isNaN( y ) ) { | |
| 88 | mStage.setX( x ); | |
| 89 | mStage.setY( y ); | |
| 90 | } // else: default behavior is center on screen | |
| 91 | ||
| 92 | if( !Double.isNaN( w ) && !Double.isNaN( h ) ) { | |
| 93 | mStage.setWidth( w ); | |
| 94 | mStage.setHeight( h ); | |
| 95 | } // else: default behavior is use scene size | |
| 96 | ||
| 97 | if( fullScreen != mStage.isFullScreen() ) { | |
| 98 | mStage.setFullScreen( fullScreen ); | |
| 99 | } | |
| 100 | ||
| 101 | if( maximized != mStage.isMaximized() ) { | |
| 102 | mStage.setMaximized( maximized ); | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Remembers the window bounds when the window is not iconified, maximized or | |
| 108 | * in fullScreen. | |
| 109 | */ | |
| 110 | private void boundsChanged() { | |
| 111 | // avoid too many (and useless) runLater() invocations | |
| 112 | if( runLaterPending ) { | |
| 113 | return; | |
| 114 | } | |
| 115 | ||
| 116 | runLaterPending = true; | |
| 117 | ||
| 118 | // must use runLater() to ensure that change of all properties | |
| 119 | // (x, y, width, height, iconified, maximized and fullScreen) | |
| 120 | // has finished | |
| 121 | Platform.runLater( () -> { | |
| 122 | runLaterPending = false; | |
| 123 | ||
| 124 | if( isNormalState() ) { | |
| 125 | normalBounds = getStageBounds(); | |
| 126 | } | |
| 127 | } ); | |
| 128 | } | |
| 129 | ||
| 130 | private boolean isNormalState() { | |
| 131 | return !mStage.isIconified() && | |
| 132 | !mStage.isMaximized() && | |
| 133 | !mStage.isFullScreen(); | |
| 134 | } | |
| 135 | ||
| 136 | private Rectangle getStageBounds() { | |
| 137 | return new Rectangle( | |
| 138 | mStage.getX(), | |
| 139 | mStage.getY(), | |
| 140 | mStage.getWidth(), | |
| 141 | mStage.getHeight() | |
| 142 | ); | |
| 143 | } | |
| 144 | } | |
| 145 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | package com.scrivenvar.util; | |
| 28 | ||
| 29 | import java.util.ArrayList; | |
| 30 | import java.util.prefs.Preferences; | |
| 31 | ||
| 32 | /** | |
| 33 | * Responsible for trimming, storing, and retrieving strings. | |
| 34 | */ | |
| 35 | public class Utils { | |
| 36 | ||
| 37 | public static String ltrim( final String s ) { | |
| 38 | int i = 0; | |
| 39 | ||
| 40 | while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) { | |
| 41 | i++; | |
| 42 | } | |
| 43 | ||
| 44 | return s.substring( i ); | |
| 45 | } | |
| 46 | ||
| 47 | public static String rtrim( final String s ) { | |
| 48 | int i = s.length() - 1; | |
| 49 | ||
| 50 | while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) { | |
| 51 | i--; | |
| 52 | } | |
| 53 | ||
| 54 | return s.substring( 0, i + 1 ); | |
| 55 | } | |
| 56 | ||
| 57 | public static String[] getPrefsStrings( final Preferences prefs, | |
| 58 | String key ) { | |
| 59 | final ArrayList<String> arr = new ArrayList<>( 256 ); | |
| 60 | ||
| 61 | for( int i = 0; i < 10000; i++ ) { | |
| 62 | final String s = prefs.get( key + (i + 1), null ); | |
| 63 | ||
| 64 | if( s == null ) { | |
| 65 | break; | |
| 66 | } | |
| 67 | ||
| 68 | arr.add( s ); | |
| 69 | } | |
| 70 | ||
| 71 | return arr.toArray( new String[ 0 ] ); | |
| 72 | } | |
| 73 | ||
| 74 | public static void putPrefsStrings( Preferences prefs, String key, | |
| 75 | String[] strings ) { | |
| 76 | for( int i = 0; i < strings.length; i++ ) { | |
| 77 | prefs.put( key + (i + 1), strings[ i ] ); | |
| 78 | } | |
| 79 | ||
| 80 | for( int i = strings.length; prefs.get( key + (i + 1), | |
| 81 | null ) != null; i++ ) { | |
| 82 | prefs.remove( key + (i + 1) ); | |
| 83 | } | |
| 84 | } | |
| 85 | } | |
| 86 | 1 |
| 1 | ||
| 1 | com.keenwrite.service.impl.DefaultOptions |
| 1 | ||
| 1 | com.keenwrite.service.impl.DefaultSettings |
| 1 | ||
| 1 | com.keenwrite.service.impl.DefaultSnitch |
| 1 | ||
| 1 | com.keenwrite.service.events.impl.DefaultNotifier |
| 1 | com.scrivenvar.service.impl.DefaultOptions | |
| 1 |
| 1 | com.scrivenvar.service.impl.DefaultSettings | |
| 1 |
| 1 | com.scrivenvar.service.impl.DefaultSnitch | |
| 1 |
| 1 | com.scrivenvar.service.events.impl.DefaultNotifier | |
| 1 |
| 1 | # Used by the Gradle build script and the application. | |
| 2 | application.title=KeenWrite | |
| 3 | ||
| 1 | 4 |
| 1 | app.properties | |
| 1 | 2 |
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | <svg | |
| 3 | xmlns:dc="http://purl.org/dc/elements/1.1/" | |
| 4 | xmlns:cc="http://creativecommons.org/ns#" | |
| 5 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |
| 6 | xmlns:svg="http://www.w3.org/2000/svg" | |
| 7 | xmlns="http://www.w3.org/2000/svg" | |
| 8 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 9 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 10 | version="1.1" | |
| 11 | width="971.53119" | |
| 12 | height="498.39355" | |
| 13 | viewBox="0 0 971.53119 498.39354" | |
| 14 | xml:space="preserve" | |
| 15 | id="svg52" | |
| 16 | sodipodi:docname="app-title.svg" | |
| 17 | inkscape:version="1.0 (4035a4fb49, 2020-05-01)" | |
| 18 | inkscape:export-filename="/home/jarvisd/dev/java/scrivenvar/docs/images/app-title.png" | |
| 19 | inkscape:export-xdpi="24.66" | |
| 20 | inkscape:export-ydpi="24.66"><metadata | |
| 21 | id="metadata56"><rdf:RDF><cc:Work | |
| 22 | rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type | |
| 23 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><sodipodi:namedview | |
| 24 | inkscape:document-rotation="0" | |
| 25 | pagecolor="#ffffff" | |
| 26 | bordercolor="#666666" | |
| 27 | borderopacity="1" | |
| 28 | objecttolerance="10" | |
| 29 | gridtolerance="10" | |
| 30 | guidetolerance="10" | |
| 31 | inkscape:pageopacity="0" | |
| 32 | inkscape:pageshadow="2" | |
| 33 | inkscape:window-width="640" | |
| 34 | inkscape:window-height="480" | |
| 35 | id="namedview54" | |
| 36 | showgrid="false" | |
| 37 | inkscape:zoom="0.78417969" | |
| 38 | inkscape:cx="455.5775" | |
| 39 | inkscape:cy="347.59625" | |
| 40 | inkscape:current-layer="svg52" | |
| 41 | fit-margin-top="0" | |
| 42 | fit-margin-left="0" | |
| 43 | fit-margin-right="0" | |
| 44 | fit-margin-bottom="0" /> | |
| 45 | <desc | |
| 46 | id="desc2">Created with Fabric.js 3.6.3</desc> | |
| 47 | <defs | |
| 48 | id="defs4"><rect | |
| 49 | x="114.92139" | |
| 50 | y="132.06313" | |
| 51 | width="470.12033" | |
| 52 | height="175.55823" | |
| 53 | id="rect933" /> | |
| 54 | ||
| 55 | ||
| 56 | ||
| 57 | ||
| 58 | ||
| 59 | ||
| 60 | ||
| 61 | ||
| 62 | ||
| 63 | ||
| 64 | ||
| 65 | ||
| 66 | <linearGradient | |
| 67 | y2="-0.049471263" | |
| 68 | x2="0.96880889" | |
| 69 | y1="-0.044911571" | |
| 70 | x1="0.15235768" | |
| 71 | gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)" | |
| 72 | gradientUnits="userSpaceOnUse" | |
| 73 | id="SVGID_1_302284"> | |
| 74 | <stop | |
| 75 | id="stop9" | |
| 76 | style="stop-color:#ec706a;stop-opacity:1" | |
| 77 | offset="0%" /> | |
| 78 | <stop | |
| 79 | id="stop11" | |
| 80 | style="stop-color:#ecd980;stop-opacity:1" | |
| 81 | offset="100%" /> | |
| 82 | </linearGradient> | |
| 83 | ||
| 84 | ||
| 85 | ||
| 86 | ||
| 87 | ||
| 88 | ||
| 89 | ||
| 90 | ||
| 91 | ||
| 92 | ||
| 93 | ||
| 94 | ||
| 95 | ||
| 96 | ||
| 97 | ||
| 98 | </defs> | |
| 99 | ||
| 100 | <g | |
| 101 | id="g853" | |
| 102 | transform="translate(-394.35834,-171.20491)"><path | |
| 103 | style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 104 | paint-order="stroke" | |
| 105 | d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534" | |
| 106 | stroke-linecap="round" | |
| 107 | id="path14" /><path | |
| 108 | style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 109 | paint-order="stroke" | |
| 110 | d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z" | |
| 111 | stroke-linecap="round" | |
| 112 | id="path22" /><path | |
| 113 | style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 114 | paint-order="stroke" | |
| 115 | d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z" | |
| 116 | stroke-linecap="round" | |
| 117 | id="path26" /><path | |
| 118 | style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 119 | paint-order="stroke" | |
| 120 | d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z" | |
| 121 | stroke-linecap="round" | |
| 122 | id="path30" /><path | |
| 123 | style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 124 | paint-order="stroke" | |
| 125 | d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z" | |
| 126 | stroke-linecap="round" | |
| 127 | id="path34" /><path | |
| 128 | style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 129 | paint-order="stroke" | |
| 130 | d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z" | |
| 131 | stroke-linecap="round" | |
| 132 | id="path38" /><path | |
| 133 | style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 134 | paint-order="stroke" | |
| 135 | d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508" | |
| 136 | stroke-linecap="round" | |
| 137 | id="path42" /></g> | |
| 138 | ||
| 139 | <text | |
| 140 | xml:space="preserve" | |
| 141 | id="text931" | |
| 142 | style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);fill:#000000;fill-opacity:1;stroke:none;" | |
| 143 | transform="translate(-394.35834,-171.20491)" /><text | |
| 144 | xml:space="preserve" | |
| 145 | style="font-style:italic;font-variant:normal;font-weight:800;font-stretch:normal;font-size:133.333px;line-height:1.25;font-family:'Merriweather Sans';-inkscape-font-specification:'Merriweather Sans, Ultra-Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#51a9cf;fill-opacity:1;stroke:none" | |
| 146 | x="311.66693" | |
| 147 | y="402.20627" | |
| 148 | id="text939"><tspan | |
| 149 | sodipodi:role="line" | |
| 150 | id="tspan937" | |
| 151 | x="311.66693" | |
| 152 | y="402.20627">KeenWrite</tspan></text></svg> | |
| 1 | 153 |
| 1 | #!/bin/bash | |
| 2 | ||
| 3 | INKSCAPE="/usr/bin/inkscape" | |
| 4 | PNG_COMPRESS="optipng" | |
| 5 | PNG_COMPRESS_OPTS="-o9 *png" | |
| 6 | ICO_TOOL="icotool" | |
| 7 | ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png" | |
| 8 | ||
| 9 | declare -a SIZES=("16" "32" "64" "128" "256" "512") | |
| 10 | ||
| 11 | for i in "${SIZES[@]}"; do | |
| 12 | # -y: export background opacity 0 | |
| 13 | $INKSCAPE -y 0 -w "${i}" --export-overwrite --export-type=png -o "logo${i}.png" "logo.svg" | |
| 14 | done | |
| 15 | ||
| 16 | # Compess the PNG images. | |
| 17 | which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS | |
| 18 | ||
| 19 | # Generate an ICO file. | |
| 20 | which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS | |
| 21 | ||
| 1 | 22 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | ||
| 29 | .markdown-editor { | |
| 30 | -fx-font-size: 11pt; | |
| 31 | } | |
| 32 | ||
| 33 | /* Subtly highlight the current paragraph. */ | |
| 34 | .markdown-editor .paragraph-box:has-caret { | |
| 35 | -fx-background-color: #fcfeff; | |
| 36 | } | |
| 37 | ||
| 38 | /* Light colour for selection highlight. */ | |
| 39 | .markdown-editor .selection { | |
| 40 | -fx-fill: #a6d2ff; | |
| 41 | } | |
| 42 | ||
| 43 | /* Decoration for words not found in the lexicon. */ | |
| 44 | .markdown-editor .spelling { | |
| 45 | -rtfx-underline-color: rgba(255, 131, 67, .7); | |
| 46 | -rtfx-underline-dash-array: 4, 2; | |
| 47 | -rtfx-underline-width: 2; | |
| 48 | -rtfx-underline-cap: round; | |
| 49 | } | |
| 1 | 50 |
| 1 | ||
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no" ?> | |
| 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |
| 3 | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1280" height="1024" viewBox="0 0 1280 1024" xml:space="preserve"> | |
| 4 | <desc>Created with Fabric.js 3.6.3</desc> | |
| 5 | <defs> | |
| 6 | </defs> | |
| 7 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0153846153846 512.012312418764)" id="background-logo" > | |
| 8 | <rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" paint-order="stroke" x="-325" y="-260" rx="0" ry="0" width="650" height="520" /> | |
| 9 | </g> | |
| 10 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 640.0170725174504 420.4016715831266)" id="logo-logo" > | |
| 11 | <g style="" paint-order="stroke" > | |
| 12 | <g transform="matrix(2.537 0 0 -2.537 -86.35385711719567 85.244912)" > | |
| 13 | <linearGradient id="SVGID_1_302284" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-24.348526 -27.478867 -27.478867 24.348526 138.479 129.67187)" x1="0" y1="0" x2="1" y2="0"> | |
| 14 | <stop offset="0%" style="stop-color:rgb(245,132,41);stop-opacity: 1"/> | |
| 15 | <stop offset="100%" style="stop-color:rgb(251,173,23);stop-opacity: 1"/> | |
| 16 | </linearGradient> | |
| 17 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: url(#SVGID_1_302284); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-127.92674550729492, -117.16399999999999)" d="m 118.951 124.648 c -9.395 -14.441 -5.243 -20.693 -5.243 -20.693 v 0 c 0 0 6.219 9.126 9.771 5.599 v 0 c 3.051 -3.023 -2.415 -8.668 -2.415 -8.668 v 0 c 0 0 33.24 13.698 17.995 28.872 v 0 c 0 0 -3.203 3.683 -7.932 3.684 v 0 c -3.46 0 -7.736 -1.97 -12.176 -8.794" stroke-linecap="round" /> | |
| 18 | </g> | |
| 19 | <g transform="matrix(2.537 0 0 -2.537 -84.52085711719567 70.2729119999999)" > | |
| 20 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(250,220,153); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(11.9895, -1.2609990716440347)" d="m 0 0 c 0 0 -6.501 6.719 -11.093 5.443 c -5.584 -1.545 -12.886 -12.078 -12.886 -12.078 c 0 0 5.98 16.932 15.29 15.731 C -1.19 8.127 0 0 0 0" stroke-linecap="round" /> | |
| 21 | </g> | |
| 22 | <g transform="matrix(2.537 0 0 -2.537 -22.327857117195663 48.729911999999956)" > | |
| 23 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-4.189, -10.432)" d="m 0 0 l -0.87 16.89 l 3.995 3.974 l 6.123 -6.156 z" stroke-linecap="round" /> | |
| 24 | </g> | |
| 25 | <g transform="matrix(2.537 0 0 -2.537 -11.3118571171957 24.124911999999966)" > | |
| 26 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(4.0955, -2.037)" d="m 0 0 l -2.081 -2.069 l -6.11 6.143 l 2.081 2.069 z" stroke-linecap="round" /> | |
| 27 | </g> | |
| 28 | <g transform="matrix(2.537 0 0 -2.537 46.27614288280432 -57.96708800000005)" > | |
| 29 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(12.070999999999998, 9.599000000000004)" d="m 0 0 c -1.226 0.69 -2.81 0.523 -3.862 -0.524 c -1.275 -1.268 -1.28 -3.33 -0.013 -4.604 l -31.681 -31.501 l -6.11 6.143 c 19.224 19.305 25.369 35.582 25.369 35.582 c 15.857 2.364 27.851 8.624 33.821 12.335 z" stroke-linecap="round" /> | |
| 30 | </g> | |
| 31 | <g transform="matrix(2.537 0 0 -2.537 -26.842857117195706 8.501911999999976)" > | |
| 32 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(4.1075, -2.0525)" d="M 0 0 L -2.081 -2.069 L -8.215 4.11 L -6.141 6.174 Z" stroke-linecap="round" /> | |
| 33 | </g> | |
| 34 | <g transform="matrix(2.537 0 0 -2.537 -51.495857117195726 19.491911999999985)" > | |
| 35 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(217,170,93); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(10.434000000000001, -1.0939999999999994)" d="m 0 0 l -3.995 -3.974 l -16.873 0.96 l 14.752 9.176 z" stroke-linecap="round" /> | |
| 36 | </g> | |
| 37 | <g transform="matrix(2.537 0 0 -2.537 55.72014288280434 -48.441088000000036)" > | |
| 38 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(201,158,82); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(9.671499999999998, 11.999499999999998)" d="M 0 0 L 17.536 17.443 C 13.788 11.486 7.47 -0.468 5.021 -16.312 c 0 0 -15.526 -6.982 -35.765 -25.13 l -6.135 6.168 l 31.681 31.5 c 1.273 -1.28 3.33 -1.279 4.604 -0.012 C 0.435 -2.764 0.629 -1.223 0 0" stroke-linecap="round" /> | |
| 39 | </g> | |
| 40 | </g> | |
| 41 | </g> | |
| 42 | <g transform="matrix(1.9692780337941629 0 0 1.9692780337941629 643.7363123827618 766.1975713477327)" id="text-logo-path" > | |
| 43 | <path style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(247,149,33); fill-rule: nonzero; opacity: 1;" paint-order="stroke" transform=" translate(-186.83999999999997, 27.08)" d="M 4.47 -6.1 L 4.47 -6.1 L 4.47 -47.5 Q 4.47 -50.27 6.43 -52.23 Q 8.39 -54.19 11.16 -54.19 L 11.16 -54.19 Q 14.01 -54.19 15.95 -52.23 Q 17.89 -50.27 17.89 -47.5 L 17.89 -47.5 L 17.89 -30.09 L 34.95 -51.97 Q 35.74 -52.97 36.94 -53.58 Q 38.13 -54.19 39.42 -54.19 L 39.42 -54.19 Q 41.77 -54.19 43.42 -52.5 Q 45.07 -50.82 45.07 -48.5 L 45.07 -48.5 Q 45.07 -46.46 43.82 -44.93 L 43.82 -44.93 L 32.93 -31.44 L 46.8 -9.81 Q 47.84 -8.11 47.84 -6.27 L 47.84 -6.27 Q 47.84 -3.33 45.9 -1.39 Q 43.96 0.55 41.19 0.55 L 41.19 0.55 Q 39.42 0.55 37.89 -0.29 Q 36.37 -1.14 35.43 -2.57 L 35.43 -2.57 L 23.78 -21.15 L 17.89 -13.9 L 17.89 -6.1 Q 17.89 -3.33 15.93 -1.39 Q 13.97 0.55 11.16 0.55 L 11.16 0.55 Q 8.39 0.55 6.43 -1.39 Q 4.47 -3.33 4.47 -6.1 Z M 50.27 -19.24 L 50.27 -19.24 Q 50.27 -25.13 52.71 -29.78 Q 55.16 -34.43 59.7 -37.06 Q 64.24 -39.69 70.27 -39.69 L 70.27 -39.69 Q 76.37 -39.69 80.78 -37.09 Q 85.18 -34.49 87.43 -30.32 Q 89.69 -26.14 89.69 -21.6 L 89.69 -21.6 Q 89.69 -18.69 88.33 -17.26 Q 86.98 -15.84 83.86 -15.84 L 83.86 -15.84 L 62.89 -15.84 Q 63.23 -12.38 65.38 -10.31 Q 67.53 -8.25 70.86 -8.25 L 70.86 -8.25 Q 72.84 -8.25 74.19 -8.91 Q 75.54 -9.57 76.62 -10.64 L 76.62 -10.64 Q 77.62 -11.58 78.42 -12.03 Q 79.22 -12.48 80.43 -12.48 L 80.43 -12.48 Q 82.61 -12.48 84.19 -10.89 Q 85.77 -9.29 85.77 -7.04 L 85.77 -7.04 Q 85.77 -4.54 83.62 -2.77 L 83.62 -2.77 Q 81.71 -1.14 78.16 -0.03 Q 74.61 1.07 70.58 1.07 L 70.58 1.07 Q 64.76 1.07 60.13 -1.42 Q 55.5 -3.92 52.89 -8.53 Q 50.27 -13.14 50.27 -19.24 Z M 62.96 -23.57 L 62.96 -23.57 L 76.96 -23.57 Q 76.82 -26.97 74.93 -28.97 Q 73.05 -30.96 70.06 -30.96 L 70.06 -30.96 Q 67.08 -30.96 65.21 -28.97 Q 63.34 -26.97 62.96 -23.57 Z M 91.63 -19.24 L 91.63 -19.24 Q 91.63 -25.13 94.07 -29.78 Q 96.52 -34.43 101.06 -37.06 Q 105.6 -39.69 111.63 -39.69 L 111.63 -39.69 Q 117.73 -39.69 122.14 -37.09 Q 126.54 -34.49 128.79 -30.32 Q 131.04 -26.14 131.04 -21.6 L 131.04 -21.6 Q 131.04 -18.69 129.69 -17.26 Q 128.34 -15.84 125.22 -15.84 L 125.22 -15.84 L 104.25 -15.84 Q 104.59 -12.38 106.74 -10.31 Q 108.89 -8.25 112.22 -8.25 L 112.22 -8.25 Q 114.2 -8.25 115.55 -8.91 Q 116.9 -9.57 117.98 -10.64 L 117.98 -10.64 Q 118.98 -11.58 119.78 -12.03 Q 120.58 -12.48 121.79 -12.48 L 121.79 -12.48 Q 123.97 -12.48 125.55 -10.89 Q 127.13 -9.29 127.13 -7.04 L 127.13 -7.04 Q 127.13 -4.54 124.98 -2.77 L 124.98 -2.77 Q 123.07 -1.14 119.52 -0.03 Q 115.96 1.07 111.94 1.07 L 111.94 1.07 Q 106.12 1.07 101.49 -1.42 Q 96.86 -3.92 94.24 -8.53 Q 91.63 -13.14 91.63 -19.24 Z M 104.32 -23.57 L 104.32 -23.57 L 118.32 -23.57 Q 118.18 -26.97 116.29 -28.97 Q 114.4 -30.96 111.42 -30.96 L 111.42 -30.96 Q 108.44 -30.96 106.57 -28.97 Q 104.7 -26.97 104.32 -23.57 Z M 135.03 -6.03 L 135.03 -6.03 L 135.03 -33.14 Q 135.03 -35.64 136.85 -37.46 Q 138.67 -39.28 141.13 -39.28 L 141.13 -39.28 Q 143.7 -39.28 145.52 -37.46 Q 147.34 -35.64 147.34 -33.14 L 147.34 -33.14 L 147.34 -32.17 Q 148.97 -35.36 152.09 -37.42 Q 155.21 -39.49 159.82 -39.49 L 159.82 -39.49 Q 166.93 -39.49 170.19 -35.47 Q 173.44 -31.44 173.44 -24.44 L 173.44 -24.44 L 173.44 -6.03 Q 173.44 -3.33 171.5 -1.39 Q 169.56 0.55 166.86 0.55 L 166.86 0.55 Q 164.15 0.55 162.19 -1.39 Q 160.24 -3.33 160.24 -6.03 L 160.24 -6.03 L 160.24 -22.36 Q 160.24 -26.35 158.54 -27.91 Q 156.84 -29.47 154.65 -29.47 L 154.65 -29.47 Q 152.02 -29.47 150.13 -27.58 Q 148.24 -25.69 148.24 -20.73 L 148.24 -20.73 L 148.24 -6.03 Q 148.24 -3.33 146.3 -1.39 Q 144.36 0.55 141.65 0.55 L 141.65 0.55 Q 138.95 0.55 136.99 -1.39 Q 135.03 -3.33 135.03 -6.03 Z M 177.71 -47.56 L 177.71 -47.56 Q 177.71 -50.34 179.63 -52.26 Q 181.56 -54.19 184.23 -54.19 L 184.23 -54.19 Q 186.58 -54.19 188.39 -52.73 Q 190.19 -51.27 190.71 -48.99 L 190.71 -48.99 L 197.88 -15.12 L 206.52 -48.64 Q 207.07 -51.07 209.12 -52.63 Q 211.16 -54.19 213.69 -54.19 L 213.69 -54.19 Q 216.26 -54.19 218.25 -52.57 Q 220.25 -50.96 220.8 -48.64 L 220.8 -48.64 L 229.4 -15.39 L 236.64 -49.33 Q 237.06 -51.38 238.76 -52.78 Q 240.46 -54.19 242.61 -54.19 L 242.61 -54.19 Q 245.17 -54.19 246.94 -52.4 Q 248.71 -50.62 248.71 -48.05 L 248.71 -48.05 Q 248.71 -47.56 248.57 -46.73 L 248.57 -46.73 L 239.69 -7.38 Q 238.9 -3.99 236.11 -1.72 Q 233.32 0.55 229.68 0.55 L 229.68 0.55 Q 226.14 0.55 223.37 -1.61 Q 220.59 -3.78 219.73 -7.11 L 219.73 -7.11 L 213.07 -33.45 L 206.38 -7.11 Q 205.51 -3.71 202.79 -1.58 Q 200.07 0.55 196.53 0.55 L 196.53 0.55 Q 192.89 0.55 190.17 -1.72 Q 187.45 -3.99 186.65 -7.38 L 186.65 -7.38 L 177.85 -46.14 Q 177.71 -47.15 177.71 -47.56 Z M 253.35 -6.03 L 253.35 -6.03 L 253.35 -33.14 Q 253.35 -35.64 255.17 -37.46 Q 256.99 -39.28 259.46 -39.28 L 259.46 -39.28 Q 262.02 -39.28 263.84 -37.46 Q 265.66 -35.64 265.66 -33.14 L 265.66 -33.14 L 265.66 -31.44 L 265.94 -31.44 Q 266.8 -33.56 268.1 -35.24 Q 269.4 -36.92 270.69 -37.61 L 270.69 -37.61 Q 271.9 -38.24 273.46 -38.27 L 273.46 -38.27 Q 276.65 -38.27 278.14 -36.45 Q 279.63 -34.63 279.63 -32.52 L 279.63 -32.52 Q 279.63 -30.33 278.11 -28.62 Q 276.58 -26.9 274.08 -26.9 L 274.08 -26.9 Q 272.59 -26.9 271.07 -26.26 Q 269.54 -25.62 268.47 -24.34 L 268.47 -24.34 Q 266.56 -21.98 266.56 -17.68 L 266.56 -17.68 L 266.56 -6.03 Q 266.56 -3.33 264.62 -1.39 Q 262.68 0.55 259.98 0.55 L 259.98 0.55 Q 257.27 0.55 255.31 -1.39 Q 253.35 -3.33 253.35 -6.03 Z M 282.41 -49.71 L 282.41 -49.71 Q 282.41 -52 284.03 -53.61 Q 285.66 -55.23 287.95 -55.23 L 287.95 -55.23 L 291.21 -55.23 Q 293.5 -55.23 295.13 -53.6 Q 296.76 -51.97 296.76 -49.71 L 296.76 -49.71 Q 296.76 -47.43 295.11 -45.8 Q 293.46 -44.17 291.21 -44.17 L 291.21 -44.17 L 287.95 -44.17 Q 285.66 -44.17 284.03 -45.8 Q 282.41 -47.43 282.41 -49.71 Z M 282.96 -6.03 L 282.96 -6.03 L 282.96 -32.66 Q 282.96 -35.36 284.92 -37.32 Q 286.88 -39.28 289.58 -39.28 L 289.58 -39.28 Q 292.29 -39.28 294.23 -37.32 Q 296.17 -35.36 296.17 -32.66 L 296.17 -32.66 L 296.17 -6.03 Q 296.17 -3.33 294.21 -1.39 Q 292.25 0.55 289.58 0.55 L 289.58 0.55 Q 286.88 0.55 284.92 -1.39 Q 282.96 -3.33 282.96 -6.03 Z M 299.43 -34.29 L 299.43 -34.29 Q 299.43 -36.12 300.71 -37.41 Q 301.99 -38.69 303.76 -38.69 L 303.76 -38.69 L 306.19 -38.69 L 306.46 -43.96 Q 306.6 -46.32 308.34 -47.98 Q 310.07 -49.64 312.5 -49.64 L 312.5 -49.64 Q 314.99 -49.64 316.76 -47.86 Q 318.53 -46.07 318.53 -43.58 L 318.53 -43.58 L 318.53 -38.69 L 322.72 -38.69 Q 324.49 -38.69 325.77 -37.41 Q 327.06 -36.12 327.06 -34.36 L 327.06 -34.36 Q 327.06 -32.52 325.77 -31.24 Q 324.49 -29.95 322.72 -29.95 L 322.72 -29.95 L 318.81 -29.95 L 318.81 -14.14 Q 318.81 -11.23 320.05 -10.02 Q 321.3 -8.81 323.83 -8.81 L 323.83 -8.81 Q 325.46 -8.46 326.61 -7.14 Q 327.75 -5.82 327.75 -4.06 L 327.75 -4.06 Q 327.75 -2.57 326.94 -1.39 Q 326.12 -0.21 324.84 0.35 L 324.84 0.35 Q 322 0.83 318.11 0.87 L 318.11 0.87 Q 311.28 0.9 308.44 -2.5 L 308.44 -2.5 Q 305.67 -5.79 305.67 -12.65 L 305.67 -12.65 Q 305.67 -12.83 305.67 -13 L 305.67 -13 L 305.74 -29.95 L 303.76 -29.95 Q 301.99 -29.95 300.71 -31.24 Q 299.43 -32.52 299.43 -34.29 Z M 329.8 -19.24 L 329.8 -19.24 Q 329.8 -25.13 332.24 -29.78 Q 334.68 -34.43 339.23 -37.06 Q 343.77 -39.69 349.8 -39.69 L 349.8 -39.69 Q 355.9 -39.69 360.3 -37.09 Q 364.71 -34.49 366.96 -30.32 Q 369.21 -26.14 369.21 -21.6 L 369.21 -21.6 Q 369.21 -18.69 367.86 -17.26 Q 366.51 -15.84 363.39 -15.84 L 363.39 -15.84 L 342.42 -15.84 Q 342.76 -12.38 344.91 -10.31 Q 347.06 -8.25 350.39 -8.25 L 350.39 -8.25 Q 352.37 -8.25 353.72 -8.91 Q 355.07 -9.57 356.14 -10.64 L 356.14 -10.64 Q 357.15 -11.58 357.95 -12.03 Q 358.74 -12.48 359.96 -12.48 L 359.96 -12.48 Q 362.14 -12.48 363.72 -10.89 Q 365.3 -9.29 365.3 -7.04 L 365.3 -7.04 Q 365.3 -4.54 363.15 -2.77 L 363.15 -2.77 Q 361.24 -1.14 357.69 -0.03 Q 354.13 1.07 350.11 1.07 L 350.11 1.07 Q 344.29 1.07 339.66 -1.42 Q 335.03 -3.92 332.41 -8.53 Q 329.8 -13.14 329.8 -19.24 Z M 342.48 -23.57 L 342.48 -23.57 L 356.49 -23.57 Q 356.35 -26.97 354.46 -28.97 Q 352.57 -30.96 349.59 -30.96 L 349.59 -30.96 Q 346.61 -30.96 344.74 -28.97 Q 342.87 -26.97 342.48 -23.57 Z" stroke-linecap="round" /> | |
| 44 | </g> | |
| 45 | </svg> |
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | <svg | |
| 3 | xmlns:dc="http://purl.org/dc/elements/1.1/" | |
| 4 | xmlns:cc="http://creativecommons.org/ns#" | |
| 5 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |
| 6 | xmlns:svg="http://www.w3.org/2000/svg" | |
| 7 | xmlns="http://www.w3.org/2000/svg" | |
| 8 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 9 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 10 | version="1.1" | |
| 11 | width="1280" | |
| 12 | height="1024" | |
| 13 | viewBox="0 0 1280 1024" | |
| 14 | xml:space="preserve" | |
| 15 | id="svg52" | |
| 16 | sodipodi:docname="logo-text.svg" | |
| 17 | inkscape:version="1.0 (4035a4fb49, 2020-05-01)"><metadata | |
| 18 | id="metadata56"><rdf:RDF><cc:Work | |
| 19 | rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type | |
| 20 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview | |
| 21 | inkscape:document-rotation="0" | |
| 22 | pagecolor="#ffffff" | |
| 23 | bordercolor="#666666" | |
| 24 | borderopacity="1" | |
| 25 | objecttolerance="10" | |
| 26 | gridtolerance="10" | |
| 27 | guidetolerance="10" | |
| 28 | inkscape:pageopacity="0" | |
| 29 | inkscape:pageshadow="2" | |
| 30 | inkscape:window-width="640" | |
| 31 | inkscape:window-height="480" | |
| 32 | id="namedview54" | |
| 33 | showgrid="false" | |
| 34 | inkscape:zoom="0.78417969" | |
| 35 | inkscape:cx="642.50039" | |
| 36 | inkscape:cy="508.59942" | |
| 37 | inkscape:current-layer="svg52" /> | |
| 38 | <desc | |
| 39 | id="desc2">Created with Fabric.js 3.6.3</desc> | |
| 40 | <defs | |
| 41 | id="defs4"><rect | |
| 42 | x="114.92139" | |
| 43 | y="132.06312" | |
| 44 | width="470.12033" | |
| 45 | height="175.55822" | |
| 46 | id="rect933" /> | |
| 47 | ||
| 48 | ||
| 49 | ||
| 50 | ||
| 51 | ||
| 52 | ||
| 53 | ||
| 54 | ||
| 55 | ||
| 56 | ||
| 57 | ||
| 58 | ||
| 59 | <linearGradient | |
| 60 | y2="-0.049471263" | |
| 61 | x2="0.96880889" | |
| 62 | y1="-0.044911571" | |
| 63 | x1="0.15235768" | |
| 64 | gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)" | |
| 65 | gradientUnits="userSpaceOnUse" | |
| 66 | id="SVGID_1_302284"> | |
| 67 | <stop | |
| 68 | id="stop9" | |
| 69 | style="stop-color:#ec706a;stop-opacity:1" | |
| 70 | offset="0%" /> | |
| 71 | <stop | |
| 72 | id="stop11" | |
| 73 | style="stop-color:#ecd980;stop-opacity:1" | |
| 74 | offset="100%" /> | |
| 75 | </linearGradient> | |
| 76 | ||
| 77 | ||
| 78 | ||
| 79 | ||
| 80 | ||
| 81 | ||
| 82 | ||
| 83 | ||
| 84 | ||
| 85 | ||
| 86 | ||
| 87 | ||
| 88 | ||
| 89 | ||
| 90 | ||
| 91 | </defs> | |
| 92 | ||
| 93 | <g | |
| 94 | id="g853"><path | |
| 95 | style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" | |
| 96 | paint-order="stroke" | |
| 97 | d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534" | |
| 98 | stroke-linecap="round" | |
| 99 | id="path14" /><path | |
| 100 | style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 101 | paint-order="stroke" | |
| 102 | d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z" | |
| 103 | stroke-linecap="round" | |
| 104 | id="path22" /><path | |
| 105 | style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 106 | paint-order="stroke" | |
| 107 | d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z" | |
| 108 | stroke-linecap="round" | |
| 109 | id="path26" /><path | |
| 110 | style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 111 | paint-order="stroke" | |
| 112 | d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z" | |
| 113 | stroke-linecap="round" | |
| 114 | id="path30" /><path | |
| 115 | style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 116 | paint-order="stroke" | |
| 117 | d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z" | |
| 118 | stroke-linecap="round" | |
| 119 | id="path34" /><path | |
| 120 | style="fill:#51a9cf;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 121 | paint-order="stroke" | |
| 122 | d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z" | |
| 123 | stroke-linecap="round" | |
| 124 | id="path38" /><path | |
| 125 | style="fill:#126d95;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;fill-opacity:1" | |
| 126 | paint-order="stroke" | |
| 127 | d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508" | |
| 128 | stroke-linecap="round" | |
| 129 | id="path42" /></g> | |
| 130 | ||
| 131 | <text | |
| 132 | xml:space="preserve" | |
| 133 | id="text931" | |
| 134 | style="fill:black;fill-opacity:1;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);" /><text | |
| 135 | xml:space="preserve" | |
| 136 | style="font-style:italic;font-variant:normal;font-weight:800;font-stretch:normal;font-size:133.333px;line-height:1.25;font-family:'Merriweather Sans';-inkscape-font-specification:'Merriweather Sans, Ultra-Bold Italic';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#51a9cf;fill-opacity:1;stroke:none;" | |
| 137 | x="311.87085" | |
| 138 | y="820.2641" | |
| 139 | id="text939"><tspan | |
| 140 | sodipodi:role="line" | |
| 141 | id="tspan937" | |
| 142 | x="311.87085" | |
| 143 | y="820.2641">KeenWrite</tspan></text></svg> | |
| 1 | 144 |
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | <svg | |
| 3 | xmlns:dc="http://purl.org/dc/elements/1.1/" | |
| 4 | xmlns:cc="http://creativecommons.org/ns#" | |
| 5 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |
| 6 | xmlns:svg="http://www.w3.org/2000/svg" | |
| 7 | xmlns="http://www.w3.org/2000/svg" | |
| 8 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 9 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 10 | inkscape:version="1.0 (4035a4fb49, 2020-05-01)" | |
| 11 | sodipodi:docname="icon.svg" | |
| 12 | id="svg52" | |
| 13 | xml:space="preserve" | |
| 14 | viewBox="0 0 512 512" | |
| 15 | height="512" | |
| 16 | width="512" | |
| 17 | version="1.1"><metadata | |
| 18 | id="metadata56"><rdf:RDF><cc:Work | |
| 19 | rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type | |
| 20 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview | |
| 21 | inkscape:current-layer="svg52" | |
| 22 | inkscape:cy="369.17559" | |
| 23 | inkscape:cx="343.24925" | |
| 24 | inkscape:zoom="0.78417969" | |
| 25 | showgrid="false" | |
| 26 | id="namedview54" | |
| 27 | inkscape:window-height="480" | |
| 28 | inkscape:window-width="640" | |
| 29 | inkscape:pageshadow="2" | |
| 30 | inkscape:pageopacity="0" | |
| 31 | guidetolerance="10" | |
| 32 | gridtolerance="10" | |
| 33 | objecttolerance="10" | |
| 34 | borderopacity="1" | |
| 35 | bordercolor="#666666" | |
| 36 | pagecolor="#ffffff" | |
| 37 | inkscape:document-rotation="0" /> | |
| 38 | <desc | |
| 39 | id="desc2">Created with Fabric.js 3.6.3</desc> | |
| 40 | <defs | |
| 41 | id="defs4"><rect | |
| 42 | id="rect933" | |
| 43 | height="175.55823" | |
| 44 | width="470.12033" | |
| 45 | y="132.06313" | |
| 46 | x="114.92139" /> | |
| 47 | ||
| 48 | ||
| 49 | ||
| 50 | ||
| 51 | ||
| 52 | ||
| 53 | ||
| 54 | ||
| 55 | ||
| 56 | ||
| 57 | ||
| 58 | ||
| 59 | <linearGradient | |
| 60 | id="SVGID_1_302284" | |
| 61 | gradientUnits="userSpaceOnUse" | |
| 62 | gradientTransform="matrix(-121.64666,137.28602,-137.28602,-121.64666,522.68198,525.78258)" | |
| 63 | x1="0.15235768" | |
| 64 | y1="-0.044911571" | |
| 65 | x2="0.96880889" | |
| 66 | y2="-0.049471263"> | |
| 67 | <stop | |
| 68 | offset="0%" | |
| 69 | style="stop-color:#ec706a;stop-opacity:1" | |
| 70 | id="stop9" /> | |
| 71 | <stop | |
| 72 | offset="100%" | |
| 73 | style="stop-color:#ecd980;stop-opacity:1" | |
| 74 | id="stop11" /> | |
| 75 | </linearGradient> | |
| 76 | ||
| 77 | ||
| 78 | ||
| 79 | ||
| 80 | ||
| 81 | ||
| 82 | ||
| 83 | ||
| 84 | ||
| 85 | ||
| 86 | ||
| 87 | ||
| 88 | ||
| 89 | ||
| 90 | ||
| 91 | </defs> | |
| 92 | ||
| 93 | <g | |
| 94 | transform="translate(-384.01706,-164.40168)" | |
| 95 | id="g853"><path | |
| 96 | id="path14" | |
| 97 | stroke-linecap="round" | |
| 98 | d="m 425.11895,550.88213 c -46.93797,72.14807 -26.19433,103.38343 -26.19433,103.38343 v 0 c 0,0 31.07048,-45.59403 48.81648,-27.97293 v 0 c 15.24298,15.10308 -12.06548,43.30583 -12.06548,43.30583 v 0 c 0,0 166.06898,-68.436 89.90407,-144.24619 v 0 c 0,0 -16.00237,-18.40049 -39.62873,-18.40548 v 0 c -17.28637,0 -38.64951,9.84223 -60.83201,43.93534" | |
| 99 | paint-order="stroke" | |
| 100 | style="fill:url(#SVGID_1_302284);fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path | |
| 101 | id="path22" | |
| 102 | stroke-linecap="round" | |
| 103 | d="m 575.11882,568.48329 -4.34657,-84.38342 19.95925,-19.85434 30.59087,30.75573 z" | |
| 104 | paint-order="stroke" | |
| 105 | style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path | |
| 106 | id="path26" | |
| 107 | stroke-linecap="round" | |
| 108 | d="m 638.20224,478.0873 -10.3968,10.33684 -30.52591,-30.69078 10.39679,-10.33685 z" | |
| 109 | paint-order="stroke" | |
| 110 | style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path | |
| 111 | id="path30" | |
| 112 | stroke-linecap="round" | |
| 113 | d="m 791.45508,258.2912 c -6.12517,-3.44728 -14.03892,-2.61294 -19.29478,2.61793 -6.36997,6.33501 -6.39495,16.63688 -0.0649,23.00186 L 613.81523,441.29182 583.28931,410.60103 c 96.04423,-96.4489 126.74501,-177.76974 126.74501,-177.76974 79.22249,-11.81068 139.14522,-43.08601 168.97169,-61.62638 z" | |
| 114 | paint-order="stroke" | |
| 115 | style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path | |
| 116 | id="path34" | |
| 117 | stroke-linecap="round" | |
| 118 | d="m 607.67733,447.39871 -10.3968,10.33684 -30.64582,-30.87064 10.36183,-10.31186 z" | |
| 119 | paint-order="stroke" | |
| 120 | style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path | |
| 121 | id="path38" | |
| 122 | stroke-linecap="round" | |
| 123 | d="m 590.73628,464.25235 -19.95925,19.85434 -84.29849,-4.79622 73.70185,-45.84383 z" | |
| 124 | paint-order="stroke" | |
| 125 | style="fill:#51a9cf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /><path | |
| 126 | id="path42" | |
| 127 | stroke-linecap="round" | |
| 128 | d="m 798.0649,265.0575 87.61088,-87.14624 c -18.72523,29.76151 -50.29032,89.4844 -62.52567,168.64194 0,0 -77.5688,34.88248 -178.68403,125.55095 L 613.81527,441.28846 772.09539,283.91262 c 6.35998,6.39496 16.63687,6.38996 23.00185,0.06 5.14095,-5.10597 6.11018,-12.8049 2.96766,-18.91508" | |
| 129 | paint-order="stroke" | |
| 130 | style="fill:#126d95;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.99606;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /></g> | |
| 131 | ||
| 132 | <text | |
| 133 | style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect933);fill:#000000;fill-opacity:1;stroke:none;" | |
| 134 | id="text931" | |
| 135 | xml:space="preserve" /></svg> | |
| 1 | 136 |
| 1 | # ######################################################################## | |
| 2 | # Main Application Window | |
| 3 | # ######################################################################## | |
| 4 | ||
| 5 | # suppress inspection "UnusedProperty" for whole file | |
| 6 | ||
| 7 | Main.menu.file=_File | |
| 8 | Main.menu.file.new=_New | |
| 9 | Main.menu.file.open=_Open... | |
| 10 | Main.menu.file.close=_Close | |
| 11 | Main.menu.file.close_all=Close All | |
| 12 | Main.menu.file.save=_Save | |
| 13 | Main.menu.file.save_as=Save _As | |
| 14 | Main.menu.file.save_all=Save A_ll | |
| 15 | Main.menu.file.exit=E_xit | |
| 16 | ||
| 17 | Main.menu.edit=_Edit | |
| 18 | Main.menu.edit.copy.html=Copy _HTML | |
| 19 | Main.menu.edit.undo=_Undo | |
| 20 | Main.menu.edit.redo=_Redo | |
| 21 | Main.menu.edit.cut=Cu_t | |
| 22 | Main.menu.edit.copy=_Copy | |
| 23 | Main.menu.edit.paste=_Paste | |
| 24 | Main.menu.edit.selectAll=Select _All | |
| 25 | Main.menu.edit.find=_Find | |
| 26 | Main.menu.edit.find.next=Find _Next | |
| 27 | Main.menu.edit.preferences=_Preferences | |
| 28 | ||
| 29 | Main.menu.insert=_Insert | |
| 30 | Main.menu.insert.blockquote=_Blockquote | |
| 31 | Main.menu.insert.code=Inline _Code | |
| 32 | Main.menu.insert.fenced_code_block=_Fenced Code Block | |
| 33 | Main.menu.insert.fenced_code_block.prompt=Enter code here | |
| 34 | Main.menu.insert.link=_Link... | |
| 35 | Main.menu.insert.image=_Image... | |
| 36 | Main.menu.insert.heading.1=Heading _1 | |
| 37 | Main.menu.insert.heading.1.prompt=heading 1 | |
| 38 | Main.menu.insert.heading.2=Heading _2 | |
| 39 | Main.menu.insert.heading.2.prompt=heading 2 | |
| 40 | Main.menu.insert.heading.3=Heading _3 | |
| 41 | Main.menu.insert.heading.3.prompt=heading 3 | |
| 42 | Main.menu.insert.unordered_list=_Unordered List | |
| 43 | Main.menu.insert.ordered_list=_Ordered List | |
| 44 | Main.menu.insert.horizontal_rule=_Horizontal Rule | |
| 45 | ||
| 46 | Main.menu.format=Forma_t | |
| 47 | Main.menu.format.bold=_Bold | |
| 48 | Main.menu.format.italic=_Italic | |
| 49 | Main.menu.format.superscript=Su_perscript | |
| 50 | Main.menu.format.subscript=Su_bscript | |
| 51 | Main.menu.format.strikethrough=Stri_kethrough | |
| 52 | ||
| 53 | Main.menu.definition=_Definition | |
| 54 | Main.menu.definition.create=_Create | |
| 55 | Main.menu.definition.insert=_Insert | |
| 56 | ||
| 57 | Main.menu.help=_Help | |
| 58 | Main.menu.help.about=About | |
| 59 | ||
| 60 | # ######################################################################## | |
| 61 | # Status Bar | |
| 62 | # ######################################################################## | |
| 63 | ||
| 64 | Main.status.text.offset=offset | |
| 65 | Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2} | |
| 66 | Main.status.state.default=OK | |
| 67 | Main.status.error.parse={0} (near ${Main.status.text.offset} {1}) | |
| 68 | Main.status.error.def.blank=Move the caret to a word before inserting a definition. | |
| 69 | Main.status.error.def.empty=Create a definition before inserting a definition. | |
| 70 | Main.status.error.def.missing=No definition value found for ''{0}''. | |
| 71 | Main.status.error.r=Error with [{0}...]: {1} | |
| 72 | Main.status.error.file.missing=Not found: {0} | |
| 73 | ||
| 74 | # ######################################################################## | |
| 75 | # Preferences | |
| 76 | # ######################################################################## | |
| 77 | ||
| 78 | Preferences.r=R | |
| 79 | Preferences.r.script=Startup Script | |
| 80 | Preferences.r.script.desc=Script runs prior to executing R statements within the document. | |
| 81 | Preferences.r.directory=Working Directory | |
| 82 | Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script. | |
| 83 | Preferences.r.delimiter.began=Delimiter Prefix | |
| 84 | Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions. | |
| 85 | Preferences.r.delimiter.ended=Delimiter Suffix | |
| 86 | Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions. | |
| 87 | ||
| 88 | Preferences.images=Images | |
| 89 | Preferences.images.directory=Relative Directory | |
| 90 | Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths. | |
| 91 | Preferences.images.suffixes=Extensions | |
| 92 | Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces. | |
| 93 | ||
| 94 | Preferences.definitions=Definitions | |
| 95 | Preferences.definitions.path=File name | |
| 96 | Preferences.definitions.path.desc=Absolute path to interpolated string definitions. | |
| 97 | Preferences.definitions.delimiter.began=Delimiter Prefix | |
| 98 | Preferences.definitions.delimiter.began.desc=Indicates when a definition key is starting. | |
| 99 | Preferences.definitions.delimiter.ended=Delimiter Suffix | |
| 100 | Preferences.definitions.delimiter.ended.desc=Indicates when a definition key is ending. | |
| 101 | ||
| 102 | Preferences.fonts=Editor | |
| 103 | Preferences.fonts.size_editor=Font Size | |
| 104 | Preferences.fonts.size_editor.desc=Font size to use for the text editor. | |
| 105 | ||
| 106 | # ######################################################################## | |
| 107 | # Definition Pane and its Tree View | |
| 108 | # ######################################################################## | |
| 109 | ||
| 110 | Definition.menu.create=Create | |
| 111 | Definition.menu.rename=Rename | |
| 112 | Definition.menu.remove=Delete | |
| 113 | Definition.menu.add.default=Undefined | |
| 114 | ||
| 115 | # ######################################################################## | |
| 116 | # Failure messages with respect to YAML files. | |
| 117 | # ######################################################################## | |
| 118 | yaml.error.open=Could not open YAML file (ensure non-empty file). | |
| 119 | yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''. | |
| 120 | yaml.error.missing=Empty definition value for key ''{0}''. | |
| 121 | yaml.error.tree.form=Unassigned definition near ''{0}''. | |
| 122 | ||
| 123 | # ######################################################################## | |
| 124 | # File Editor | |
| 125 | # ######################################################################## | |
| 126 | ||
| 127 | FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1} | |
| 128 | FileEditor.loadFailed.title=Load | |
| 129 | FileEditor.loadFailed.reason.permissions=File must be readable and writable. | |
| 130 | FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1} | |
| 131 | FileEditor.saveFailed.title=Save | |
| 132 | ||
| 133 | # ######################################################################## | |
| 134 | # File Open | |
| 135 | # ######################################################################## | |
| 136 | ||
| 137 | Dialog.file.choose.open.title=Open File | |
| 138 | Dialog.file.choose.save.title=Save File | |
| 139 | ||
| 140 | Dialog.file.choose.filter.title.source=Source Files | |
| 141 | Dialog.file.choose.filter.title.definition=Definition Files | |
| 142 | Dialog.file.choose.filter.title.xml=XML Files | |
| 143 | Dialog.file.choose.filter.title.all=All Files | |
| 144 | ||
| 145 | # ######################################################################## | |
| 146 | # Alert Dialog | |
| 147 | # ######################################################################## | |
| 148 | ||
| 149 | Alert.file.close.title=Close | |
| 150 | Alert.file.close.text=Save changes to {0}? | |
| 151 | ||
| 152 | # ######################################################################## | |
| 153 | # Definition Pane | |
| 154 | # ######################################################################## | |
| 155 | ||
| 156 | Pane.definition.node.root.title=Definitions | |
| 157 | Pane.definition.button.create.label=_Create | |
| 158 | Pane.definition.button.rename.label=_Rename | |
| 159 | Pane.definition.button.delete.label=_Delete | |
| 160 | Pane.definition.button.create.tooltip=Add new item (Insert) | |
| 161 | Pane.definition.button.rename.tooltip=Rename selected item (F2) | |
| 162 | Pane.definition.button.delete.tooltip=Delete selected items (Delete) | |
| 163 | ||
| 164 | # Controls ############################################################### | |
| 165 | ||
| 166 | # ######################################################################## | |
| 167 | # Browse File | |
| 168 | # ######################################################################## | |
| 169 | ||
| 170 | BrowseFileButton.chooser.title=Browse for local file | |
| 171 | BrowseFileButton.chooser.allFilesFilter=All Files | |
| 172 | BrowseFileButton.tooltip=${BrowseFileButton.chooser.title} | |
| 173 | ||
| 174 | # Dialogs ################################################################ | |
| 175 | ||
| 176 | # ######################################################################## | |
| 177 | # Image | |
| 178 | # ######################################################################## | |
| 179 | ||
| 180 | Dialog.image.title=Image | |
| 181 | Dialog.image.chooser.imagesFilter=Images | |
| 182 | Dialog.image.previewLabel.text=Markdown Preview\: | |
| 183 | Dialog.image.textLabel.text=Alternate Text\: | |
| 184 | Dialog.image.titleLabel.text=Title (tooltip)\: | |
| 185 | Dialog.image.urlLabel.text=Image URL\: | |
| 186 | ||
| 187 | # ######################################################################## | |
| 188 | # Hyperlink | |
| 189 | # ######################################################################## | |
| 190 | ||
| 191 | Dialog.link.title=Link | |
| 192 | Dialog.link.previewLabel.text=Markdown Preview\: | |
| 193 | Dialog.link.textLabel.text=Link Text\: | |
| 194 | Dialog.link.titleLabel.text=Title (tooltip)\: | |
| 195 | Dialog.link.urlLabel.text=Link URL\: | |
| 196 | ||
| 197 | # ######################################################################## | |
| 198 | # About | |
| 199 | # ######################################################################## | |
| 200 | ||
| 201 | Dialog.about.title=About {0} | |
| 202 | Dialog.about.header={0} | |
| 203 | Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber | |
| 1 | 204 |
| 1 | /* RESET ***/ | |
| 2 | html{box-sizing:border-box;font-size:12pt}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0} | |
| 3 | ||
| 4 | /* BODY ***/ | |
| 5 | body { | |
| 6 | /* Must be bundled in JAR file. */ | |
| 7 | font-family: "Vollkorn", serif; | |
| 8 | background-color: #fff; | |
| 9 | margin: 0 auto; | |
| 10 | max-width: 960px; | |
| 11 | line-height: 1.6; | |
| 12 | color: #454545; | |
| 13 | padding: 1em; | |
| 14 | font-feature-settings: "liga" 1; | |
| 15 | font-variant-ligatures: normal; | |
| 16 | } | |
| 17 | ||
| 18 | body>*:first-child { | |
| 19 | margin-top: 0 !important; | |
| 20 | } | |
| 21 | ||
| 22 | body>*:last-child { | |
| 23 | margin-bottom: 0 !important; | |
| 24 | } | |
| 25 | ||
| 26 | /* BLOCKS ***/ | |
| 27 | p, blockquote, ul, ol, dl, table, pre { | |
| 28 | margin: 1em 0; | |
| 29 | } | |
| 30 | ||
| 31 | /* HEADINGS ***/ | |
| 32 | h1, h2, h3, h4, h5, h6 { | |
| 33 | font-weight: bold; | |
| 34 | margin: 1em 0 .5em; | |
| 35 | } | |
| 36 | ||
| 37 | h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, | |
| 38 | h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code { | |
| 39 | font-size: inherit; | |
| 40 | } | |
| 41 | ||
| 42 | h1 { | |
| 43 | font-size: 21pt; | |
| 44 | } | |
| 45 | ||
| 46 | h2 { | |
| 47 | font-size: 18pt; | |
| 48 | border-bottom: 1px solid #ccc; | |
| 49 | } | |
| 50 | ||
| 51 | h3 { | |
| 52 | font-size: 15pt; | |
| 53 | } | |
| 54 | ||
| 55 | h4 { | |
| 56 | font-size: 13.5pt; | |
| 57 | } | |
| 58 | ||
| 59 | h5 { | |
| 60 | font-size: 12pt; | |
| 61 | } | |
| 62 | ||
| 63 | h6 { | |
| 64 | font-size: 10.5pt; | |
| 65 | } | |
| 66 | ||
| 67 | h1+p, h2+p, h3+p, h4+p, h5+p, h6+p { | |
| 68 | margin-top: .5em; | |
| 69 | } | |
| 70 | ||
| 71 | /* LINKS ***/ | |
| 72 | a { | |
| 73 | color: #0077aa; | |
| 74 | text-decoration: none; | |
| 75 | } | |
| 76 | ||
| 77 | a:hover { | |
| 78 | text-decoration: underline; | |
| 79 | } | |
| 80 | ||
| 81 | /* BULLET LISTS ***/ | |
| 82 | ul, ol { | |
| 83 | display: block; | |
| 84 | list-style: disc outside none; | |
| 85 | margin: 1em 0; | |
| 86 | padding: 0 0 0 2em; | |
| 87 | } | |
| 88 | ||
| 89 | ol { | |
| 90 | list-style-type: decimal; | |
| 91 | } | |
| 92 | ||
| 93 | ul ul, ol ul, | |
| 94 | ol ol, ul ol { | |
| 95 | list-style-position: inside; | |
| 96 | margin-left: 1em; | |
| 97 | } | |
| 98 | ||
| 99 | ul ul, ol ul { | |
| 100 | list-style-type: circle; | |
| 101 | } | |
| 102 | ||
| 103 | ol ol, ul ol { | |
| 104 | list-style-type: lower-latin; | |
| 105 | } | |
| 106 | ||
| 107 | /* DEFINITION LISTS ***/ | |
| 108 | dl { | |
| 109 | /** Horizontal scroll bar will appear if set to 100%. */ | |
| 110 | width: 99%; | |
| 111 | overflow: hidden; | |
| 112 | padding-left: 1em; | |
| 113 | } | |
| 114 | ||
| 115 | dl dt { | |
| 116 | font-weight: bold; | |
| 117 | float: left; | |
| 118 | width: 20%; | |
| 119 | clear: both; | |
| 120 | position: relative; | |
| 121 | } | |
| 122 | ||
| 123 | dl dd { | |
| 124 | float: right; | |
| 125 | width: 79%; | |
| 126 | padding-bottom: .5em; | |
| 127 | margin-left: 0; | |
| 128 | } | |
| 129 | ||
| 130 | /* CODE ***/ | |
| 131 | pre, code, tt { | |
| 132 | /* Must be bundled in JAR file. */ | |
| 133 | font-family: "Fira Code", monospace; | |
| 134 | font-size: 10pt; | |
| 135 | background-color: #f8f8f8; | |
| 136 | text-decoration: none; | |
| 137 | white-space: pre-wrap; | |
| 138 | word-wrap: break-word; | |
| 139 | overflow-wrap: anywhere; | |
| 140 | border-radius: .125em; | |
| 141 | } | |
| 142 | ||
| 143 | code, tt { | |
| 144 | padding: .25em; | |
| 145 | } | |
| 146 | ||
| 147 | pre > code { | |
| 148 | /* Reset the padding. */ | |
| 149 | padding: 0; | |
| 150 | border: none; | |
| 151 | background: transparent; | |
| 152 | } | |
| 153 | ||
| 154 | pre { | |
| 155 | border: .125em solid #ccc; | |
| 156 | overflow: auto; | |
| 157 | /* Assign the new padding, independently from previous. */ | |
| 158 | padding: .25em .5em; | |
| 159 | } | |
| 160 | ||
| 161 | pre code, pre tt { | |
| 162 | background-color: transparent; | |
| 163 | border: none; | |
| 164 | } | |
| 165 | ||
| 166 | /* QUOTES ***/ | |
| 167 | blockquote { | |
| 168 | border-left: .25em solid #ccc; | |
| 169 | padding: 0 1em; | |
| 170 | color: #777; | |
| 171 | } | |
| 172 | ||
| 173 | blockquote>:first-child { | |
| 174 | margin-top: 0; | |
| 175 | } | |
| 176 | ||
| 177 | blockquote>:last-child { | |
| 178 | margin-bottom: 0; | |
| 179 | } | |
| 180 | ||
| 181 | /* HORIZONTAL RULES ***/ | |
| 182 | hr { | |
| 183 | clear: both; | |
| 184 | margin: 1.5em 0 1.5em; | |
| 185 | height: 0; | |
| 186 | overflow: hidden; | |
| 187 | border: none; | |
| 188 | background: transparent; | |
| 189 | border-bottom: .125em solid #ccc; | |
| 190 | } | |
| 191 | ||
| 192 | /* TABLES ***/ | |
| 193 | table { | |
| 194 | width: 100%; | |
| 195 | } | |
| 196 | ||
| 197 | tr:nth-child(odd) { | |
| 198 | background-color: #eee; | |
| 199 | } | |
| 200 | ||
| 201 | th { | |
| 202 | background-color: #454545; | |
| 203 | color: #fff; | |
| 204 | } | |
| 205 | ||
| 206 | th, td { | |
| 207 | text-align: left; | |
| 208 | padding: 0 1em; | |
| 209 | } | |
| 210 | ||
| 211 | /* IMAGES ***/ | |
| 212 | img { | |
| 213 | max-width: 100%; | |
| 214 | } | |
| 215 | ||
| 216 | /* Required for FlyingSaucer to detect the node. | |
| 217 | * See SVGReplacedElementFactory for details. | |
| 218 | */ | |
| 219 | tex { | |
| 220 | /* Ensure the formulas can be inlined with text. */ | |
| 221 | display: inline-block; | |
| 222 | } | |
| 223 | ||
| 224 | /* Without a robust typesetting engine, there's no | |
| 225 | * nice-looking way to automatically typeset equations. | |
| 226 | * Sometimes baseline is appropriate, sometimes the | |
| 227 | * descender must be considered, and sometimes vertical | |
| 228 | * alignment to the middle looks best. | |
| 229 | */ | |
| 230 | p tex { | |
| 231 | vertical-align: baseline; | |
| 232 | } | |
| 1 | 233 |
| 1 | /* | |
| 2 | * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | /*---- toolbar ----*/ | |
| 29 | ||
| 30 | .tool-bar { | |
| 31 | -fx-spacing: 0; | |
| 32 | } | |
| 33 | ||
| 34 | .tool-bar .button { | |
| 35 | -fx-background-color: transparent; | |
| 36 | } | |
| 37 | ||
| 38 | .tool-bar .button:hover { | |
| 39 | -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; | |
| 40 | -fx-color: -fx-hover-base; | |
| 41 | } | |
| 42 | ||
| 43 | .tool-bar .button:armed { | |
| 44 | -fx-color: -fx-pressed-base; | |
| 45 | } | |
| 1 | 46 |
| 1 | # ######################################################################## | |
| 2 | # Application | |
| 3 | # ######################################################################## | |
| 4 | ||
| 5 | application.title=keenwrite | |
| 6 | application.package=com/${application.title} | |
| 7 | application.messages= com.${application.title}.messages | |
| 8 | ||
| 9 | # Suppress multiple file modified notifications for one logical modification. | |
| 10 | # Given in milliseconds. | |
| 11 | application.watchdog.timeout=50 | |
| 12 | ||
| 13 | # ######################################################################## | |
| 14 | # Preferences | |
| 15 | # ######################################################################## | |
| 16 | ||
| 17 | preferences.root=com.${application.title} | |
| 18 | preferences.root.state=state | |
| 19 | preferences.root.options=options | |
| 20 | preferences.root.definition.source=definition.source | |
| 21 | ||
| 22 | # ######################################################################## | |
| 23 | # File and Path References | |
| 24 | # ######################################################################## | |
| 25 | file.stylesheet.scene=${application.package}/scene.css | |
| 26 | file.stylesheet.markdown=${application.package}/editor/markdown.css | |
| 27 | file.stylesheet.preview=webview.css | |
| 28 | file.stylesheet.xml=${application.package}/xml.css | |
| 29 | ||
| 30 | file.logo.16 =${application.package}/logo16.png | |
| 31 | file.logo.32 =${application.package}/logo32.png | |
| 32 | file.logo.128=${application.package}/logo128.png | |
| 33 | file.logo.256=${application.package}/logo256.png | |
| 34 | file.logo.512=${application.package}/logo512.png | |
| 35 | ||
| 36 | # Default file name when a new file is created. | |
| 37 | # This ensures that the file type can always be | |
| 38 | # discerned so that the correct type of variable | |
| 39 | # reference can be inserted. | |
| 40 | file.default=untitled.md | |
| 41 | file.definition.default=variables.yaml | |
| 42 | ||
| 43 | # ######################################################################## | |
| 44 | # File name Extensions | |
| 45 | # ######################################################################## | |
| 46 | ||
| 47 | # Comma-separated list of definition file name extensions. | |
| 48 | definition.file.ext.json=*.json | |
| 49 | definition.file.ext.toml=*.toml | |
| 50 | definition.file.ext.yaml=*.yml,*.yaml | |
| 51 | definition.file.ext.properties=*.properties,*.props | |
| 52 | ||
| 53 | # Comma-separated list of file name extensions. | |
| 54 | file.ext.rmarkdown=*.Rmd | |
| 55 | file.ext.rxml=*.Rxml | |
| 56 | file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml} | |
| 57 | file.ext.definition=${definition.file.ext.yaml} | |
| 58 | file.ext.xml=*.xml,${file.ext.rxml} | |
| 59 | file.ext.all=*.* | |
| 60 | ||
| 61 | # File name extension search order for images. | |
| 62 | file.ext.image.order=svg pdf png jpg tiff | |
| 63 | ||
| 64 | # ######################################################################## | |
| 65 | # Variable Name Editor | |
| 66 | # ######################################################################## | |
| 67 | ||
| 68 | # Maximum number of characters for a variable name. A variable is defined | |
| 69 | # as one or more non-whitespace characters up to this maximum length. | |
| 70 | editor.variable.maxLength=256 | |
| 71 | ||
| 72 | # ######################################################################## | |
| 73 | # Dialog Preferences | |
| 74 | # ######################################################################## | |
| 75 | ||
| 76 | dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R | |
| 77 | dialog.alert.button.order.linux=L_HE+UNYACBXIO_R | |
| 78 | dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R | |
| 79 | ||
| 80 | # Ensures a consistent button order for alert dialogs across platforms (because | |
| 81 | # the default button order on Linux defies all logic). | |
| 82 | dialog.alert.button.order=${dialog.alert.button.order.windows} | |
| 1 | 83 |
| 1 | --- | |
| 2 | c: | |
| 3 | protagonist: | |
| 4 | name: | |
| 5 | First: Chloe | |
| 6 | First_pos: $c.protagonist.name.First$'s | |
| 7 | Middle: Irene | |
| 8 | Family: Angelos | |
| 9 | nick: | |
| 10 | Father: Savant | |
| 11 | Mother: Sweetie | |
| 12 | colour: | |
| 13 | eyes: green | |
| 14 | hair: dark auburn | |
| 15 | syn_1: black | |
| 16 | syn_2: purple | |
| 17 | syn_11: teal | |
| 18 | syn_6: silver | |
| 19 | favourite: emerald green | |
| 20 | speech: | |
| 21 | tic: oh | |
| 22 | father: | |
| 23 | heritage: Greek | |
| 24 | name: | |
| 25 | Short: Bryce | |
| 26 | First: Bryson | |
| 27 | First_pos: $c.protagonist.father.name.First$'s | |
| 28 | Honourific: Mr. | |
| 29 | education: Masters | |
| 30 | vocation: | |
| 31 | name: robotics | |
| 32 | title: roboticist | |
| 33 | employer: | |
| 34 | name: | |
| 35 | Short: Rabota | |
| 36 | Full: $c.protagonist.father.employer.name.Short$ Designs | |
| 37 | hair: | |
| 38 | style: thick, curly | |
| 39 | colour: black | |
| 40 | eyes: | |
| 41 | colour: dark brown | |
| 42 | Endear: Dad | |
| 43 | vehicle: coupé | |
| 44 | mother: | |
| 45 | name: | |
| 46 | Short: Cass | |
| 47 | First: Cassandra | |
| 48 | First_pos: $c.protagonist.mother.name.First$'s | |
| 49 | Honourific: Mrs. | |
| 50 | education: PhD | |
| 51 | speech: | |
| 52 | tic: cute | |
| 53 | Honorific: Doctor | |
| 54 | vocation: | |
| 55 | article: an | |
| 56 | name: oceanography | |
| 57 | title: oceanographer | |
| 58 | employer: | |
| 59 | name: | |
| 60 | Full: Oregon State University | |
| 61 | Short: OSU | |
| 62 | eyes: | |
| 63 | colour: blue | |
| 64 | hair: | |
| 65 | style: thick, curly | |
| 66 | colour: dark brown | |
| 67 | Endear: Mom | |
| 68 | Endear_pos: Mom's | |
| 69 | uncle: | |
| 70 | name: | |
| 71 | First: Damian | |
| 72 | First_pos: $c.protagonist.uncle.name.First$'s | |
| 73 | Family: Moros | |
| 74 | hands: | |
| 75 | fingers: | |
| 76 | shape: long, bony | |
| 77 | friend: | |
| 78 | primary: | |
| 79 | name: | |
| 80 | First: Gerard | |
| 81 | First_pos: $c.protagonist.friend.primary.name.First$'s | |
| 82 | Family: Baran | |
| 83 | Family_pos: $c.protagonist.friend.primary.name.Family$'s | |
| 84 | favourite: | |
| 85 | colour: midnight blue | |
| 86 | eyes: | |
| 87 | colour: hazel | |
| 88 | mother: | |
| 89 | name: | |
| 90 | First: Isabella | |
| 91 | Short: Izzy | |
| 92 | Honourific: Mrs. | |
| 93 | father: | |
| 94 | name: | |
| 95 | Short: Mo | |
| 96 | First: Montgomery | |
| 97 | First_pos: $c.protagonist.friend.primary.father.name.First$'s | |
| 98 | Honourific: Mr. | |
| 99 | speech: | |
| 100 | tic: y'know | |
| 101 | endear: Pops | |
| 102 | military: | |
| 103 | primary: | |
| 104 | name: | |
| 105 | First: Felix | |
| 106 | Family: LeMay | |
| 107 | Family_pos: LeMay's | |
| 108 | rank: | |
| 109 | Short: General | |
| 110 | Full: Brigadier $c.military.primary.rank.Short$ | |
| 111 | colour: | |
| 112 | eyes: gray | |
| 113 | hair: dirty brown | |
| 114 | secondary: | |
| 115 | name: | |
| 116 | Family: Grell | |
| 117 | rank: Colonel | |
| 118 | colour: | |
| 119 | eyes: green | |
| 120 | hair: deep red | |
| 121 | quaternary: | |
| 122 | name: | |
| 123 | First: Gretchen | |
| 124 | Family: Steinherz | |
| 125 | minor: | |
| 126 | primary: | |
| 127 | name: | |
| 128 | First: River | |
| 129 | Family: Banks | |
| 130 | Honourific: Mx. | |
| 131 | vocation: | |
| 132 | title: salesperson | |
| 133 | employer: | |
| 134 | Name: Geophysical Prospecting Incorporated | |
| 135 | Abbr: GPI | |
| 136 | Area: Cold Spring Creek | |
| 137 | payment: twenty million | |
| 138 | secondary: | |
| 139 | name: | |
| 140 | First: Renato | |
| 141 | Middle: Carroña | |
| 142 | Family: Salvatierra | |
| 143 | Family_pos: $c.minor.secondary.name.Family$'s | |
| 144 | Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$ | |
| 145 | Honourific: Mister | |
| 146 | Honourific_sp: Señor | |
| 147 | vocation: | |
| 148 | title: detective | |
| 149 | tertiary: | |
| 150 | name: | |
| 151 | First: Robert | |
| 152 | Family: Hanssen | |
| 153 | ||
| 154 | ai: | |
| 155 | protagonist: | |
| 156 | name: | |
| 157 | first: yoky | |
| 158 | First: Yoky | |
| 159 | First_pos: $c.ai.protagonist.name.First$'s | |
| 160 | Family: Tsukuda | |
| 161 | id: 46692 | |
| 162 | persona: | |
| 163 | name: | |
| 164 | First: Hoshi | |
| 165 | First_pos: $c.ai.protagonist.persona.name.First$'s | |
| 166 | Family: Yamamoto | |
| 167 | Family_pos: $c.ai.protagonist.persona.name.Family$'s | |
| 168 | culture: Japanese-American | |
| 169 | ethnicity: Asian | |
| 170 | rank: Technical Sergeant | |
| 171 | speech: | |
| 172 | tic: okay | |
| 173 | first: | |
| 174 | Name: Prôtos | |
| 175 | Name_pos: Prôtos' | |
| 176 | age: | |
| 177 | actual: twenty-six weeks | |
| 178 | virtual: five years | |
| 179 | second: | |
| 180 | Name: Défteros | |
| 181 | third: | |
| 182 | Name: Trítos | |
| 183 | fourth: | |
| 184 | Name: Tétartos | |
| 185 | material: | |
| 186 | type: metal | |
| 187 | raw: ilmenite | |
| 188 | extract: ore | |
| 189 | name: | |
| 190 | short: titanium | |
| 191 | long: $c.ai.material.name.short$ dioxide | |
| 192 | Abbr: TiO~2~ | |
| 193 | pejorative: tin | |
| 194 | animal: | |
| 195 | protagonist: | |
| 196 | Name: Trufflers | |
| 197 | type: pig | |
| 198 | antagonist: | |
| 199 | name: coywolf | |
| 200 | Name: Coywolf | |
| 201 | plural: coywolves | |
| 202 | ||
| 203 | narrator: | |
| 204 | one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$) | |
| 205 | two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$) | |
| 206 | ||
| 207 | military: | |
| 208 | name: | |
| 209 | Short: Agency | |
| 210 | Short_pos: $military.name.Short$'s | |
| 211 | plural: agencies | |
| 212 | machine: | |
| 213 | Name: Skopós | |
| 214 | Name_pos: $military.machine.Name$' | |
| 215 | Location: Arctic | |
| 216 | predictor: quantum chips | |
| 217 | land: | |
| 218 | name: | |
| 219 | Full: $military.name.Short$ of Defence | |
| 220 | Slogan: Safety in Numbers | |
| 221 | air: | |
| 222 | name: | |
| 223 | Full: $military.name.Short$ of Air | |
| 224 | compound: | |
| 225 | type: base | |
| 226 | lights: | |
| 227 | colour: blue | |
| 228 | nick: | |
| 229 | Prefix: Catacombs | |
| 230 | prep: of | |
| 231 | Suffix: Tartarus | |
| 232 | ||
| 233 | government: | |
| 234 | Country: United States | |
| 235 | ||
| 236 | location: | |
| 237 | protagonist: | |
| 238 | City: Corvallis | |
| 239 | Region: Oregon | |
| 240 | Geography: Willamette Valley | |
| 241 | secondary: | |
| 242 | City: Willow Branch Spring | |
| 243 | Region: Oregon | |
| 244 | Geography: Wheeler County | |
| 245 | Water: Clarno Rapids | |
| 246 | Road: Shaniko-Fossil Highway | |
| 247 | tertiary: | |
| 248 | City: Leavenworth | |
| 249 | Region: Washington | |
| 250 | Type: Bavarian village | |
| 251 | school: | |
| 252 | address: 1400 Northwest Buchanan Avenue | |
| 253 | hospital: | |
| 254 | Name: Good Samaritan Regional Medical Center | |
| 255 | ai: | |
| 256 | escape: | |
| 257 | country: | |
| 258 | Name: Ecuador | |
| 259 | Name_pos: Ecuador's | |
| 260 | mountain: | |
| 261 | Name: Chimborazo | |
| 262 | ||
| 263 | language: | |
| 264 | ai: | |
| 265 | article: an | |
| 266 | singular: exanimis | |
| 267 | plural: exanimēs | |
| 268 | brain: | |
| 269 | singular: superum | |
| 270 | plural: supera | |
| 271 | title: memristor array | |
| 272 | Title: Memristor Array | |
| 273 | police: | |
| 274 | slang: | |
| 275 | singular: mippo | |
| 276 | plural: $language.police.slang.singular$s | |
| 277 | ||
| 278 | date: | |
| 279 | anchor: 2042-09-02 | |
| 280 | protagonist: | |
| 281 | born: 0 | |
| 282 | conceived: -243 | |
| 283 | attacked: | |
| 284 | first: 2192 | |
| 285 | second: 8064 | |
| 286 | father: | |
| 287 | attacked: | |
| 288 | first: -8205 | |
| 289 | date: | |
| 290 | second: -1550 | |
| 291 | family: | |
| 292 | moved: | |
| 293 | first: $date.protagonist.conceived$ + 35 | |
| 294 | game: | |
| 295 | played: | |
| 296 | first: $date.protagonist.born$ - 672 | |
| 297 | second: $date.protagonist.family.moved.first$ + 2 | |
| 298 | ai: | |
| 299 | interviewed: 6198 | |
| 300 | onboarded: $date.ai.interviewed$ + 290 | |
| 301 | diagnosed: $date.ai.onboarded$ + 2 | |
| 302 | resigned: $date.ai.diagnosed$ + 3 | |
| 303 | trapped: $date.ai.resigned$ + 26 | |
| 304 | torturer: $date.ai.trapped$ + 18 | |
| 305 | memristor: $date.ai.torturer$ + 61 | |
| 306 | ethics: $date.ai.memristor$ + 415 | |
| 307 | trained: $date.ai.ethics$ + 385 | |
| 308 | mindjacked: $date.ai.trained$ + 22 | |
| 309 | bombed: $date.ai.mindjacked$ + 458 | |
| 310 | military: | |
| 311 | machine: | |
| 312 | Construction: Six years | |
| 313 | ||
| 314 | plot: | |
| 315 | Log: $c.ai.protagonist.name.First_pos$ Chronicles | |
| 316 | Channel: Quantum Channel | |
| 317 | ||
| 318 | device: | |
| 319 | computer: | |
| 320 | Name: Tau | |
| 321 | network: | |
| 322 | Name: Internet | |
| 323 | paper: | |
| 324 | name: | |
| 325 | full: electronic sheet | |
| 326 | short: sheet | |
| 327 | typewriter: | |
| 328 | Name: Underwood | |
| 329 | year: nineteen twenties | |
| 330 | room: root cellar | |
| 331 | portable: | |
| 332 | name: nanobook | |
| 333 | vehicle: | |
| 334 | name: robocars | |
| 335 | Name: Robocars | |
| 336 | sensor: | |
| 337 | name: BMP1580 | |
| 338 | phone: | |
| 339 | name: comm | |
| 340 | name_pos: $plot.device.phone.name$'s | |
| 341 | Name: Comm | |
| 342 | plural: $plot.device.phone.name$s | |
| 343 | video: | |
| 344 | name: vidfeed | |
| 345 | plural: $plot.device.video.name$s | |
| 346 | game: | |
| 347 | Name: Psynæris | |
| 348 | thought: transed | |
| 349 | machine: telecognos | |
| 350 | location: | |
| 351 | Building: Nijō Castle | |
| 352 | District: Gion | |
| 353 | City: Kyoto | |
| 354 | Country: Japan | |
| 355 | ||
| 356 | farm: | |
| 357 | population: | |
| 358 | estimate: 350 | |
| 359 | actual: 1,000 | |
| 360 | energy: 9800kJ | |
| 361 | width: 55m | |
| 362 | length: 55m | |
| 363 | storeys: 10 | |
| 364 | ||
| 365 | lamp: | |
| 366 | height: 0.17m | |
| 367 | length: 1.22m | |
| 368 | width: 0.28m | |
| 369 | ||
| 370 | crop: | |
| 371 | name: | |
| 372 | singular: tomato | |
| 373 | plural: $crop.name.singular$es | |
| 374 | energy: 318kJ | |
| 375 | weight: 450g | |
| 376 | yield: 50 | |
| 377 | harvests: 7 | |
| 378 | diameter: 2m | |
| 379 | height: 1.5m | |
| 380 | ||
| 381 | heading: | |
| 382 | ch_01: Till | |
| 383 | ch_02: Sow | |
| 384 | ch_03: Seed | |
| 385 | ch_04: Germinate | |
| 386 | ch_05: Grow | |
| 387 | ch_06: Shoot | |
| 388 | ch_07: Bud | |
| 389 | ch_08: Bloom | |
| 390 | ch_09: Pollinate | |
| 391 | ch_10: Fruit | |
| 392 | ch_11: Harvest | |
| 393 | ch_12: Deliver | |
| 394 | ch_13: Spoil | |
| 395 | ch_14: Revolt | |
| 396 | ch_15: Compost | |
| 397 | ch_16: Burn | |
| 398 | ch_17: Release | |
| 399 | ch_18: End Notes | |
| 400 | ch_19: Characters | |
| 401 | ||
| 402 | inference: | |
| 403 | unit: per cent | |
| 404 | min: two | |
| 405 | ch_sow: eighty | |
| 406 | ch_seed: fifty-two | |
| 407 | ch_germinate: thirty-one | |
| 408 | ch_grow: fifteen | |
| 409 | ch_shoot: seven | |
| 410 | ch_bloom: four | |
| 411 | ch_pollinate: two | |
| 412 | ch_harvest: ninety-five | |
| 413 | ch_delivery: ninety-eight | |
| 414 | ||
| 415 | link: | |
| 416 | tartarus: https://en.wikipedia.org/wiki/Tartarus | |
| 417 | exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls | |
| 418 | atalanta: https://en.wikipedia.org/wiki/Atalanta | |
| 419 | detain: https://goo.gl/RCNuOQ | |
| 420 | ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics | |
| 421 | algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon | |
| 422 | holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust | |
| 423 | memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf | |
| 424 | surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487 | |
| 425 | tor: https://www.torproject.org | |
| 426 | hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra | |
| 427 | foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134 | |
| 428 | drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist | |
| 429 | fermi: https://arxiv.org/pdf/1404.0204v1.pdf | |
| 430 | face: https://www.youtube.com/watch?v=ladqJQLR2bA | |
| 431 | expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures | |
| 432 | governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531 | |
| 433 | asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics | |
| 434 | clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws | |
| 435 | jetpack: http://jetpackaviation.com/ | |
| 436 | hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ | |
| 437 | eyes_five: https://en.wikipedia.org/wiki/Five_Eyes | |
| 438 | eyes_nine: https://www.privacytools.io/ | |
| 439 | eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html | |
| 440 | tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml | |
| 441 | ||
| 1 | 442 |
| 1 | .tagmark { | |
| 2 | -fx-fill: gray; | |
| 3 | } | |
| 4 | .anytag { | |
| 5 | -fx-fill: crimson; | |
| 6 | } | |
| 7 | .paren { | |
| 8 | -fx-fill: firebrick; | |
| 9 | -fx-font-weight: bold; | |
| 10 | } | |
| 11 | .attribute { | |
| 12 | -fx-fill: darkviolet; | |
| 13 | } | |
| 14 | .avalue { | |
| 15 | -fx-fill: black; | |
| 16 | } | |
| 1 | 17 | |
| 18 | .comment { | |
| 19 | -fx-fill: teal; | |
| 20 | } |
| 1 | app.properties | |
| 2 | 1 |
| 1 | #!/bin/bash | |
| 2 | ||
| 3 | INKSCAPE="/usr/bin/inkscape" | |
| 4 | PNG_COMPRESS="optipng" | |
| 5 | PNG_COMPRESS_OPTS="-o9 *png" | |
| 6 | ICO_TOOL="icotool" | |
| 7 | ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png" | |
| 8 | ||
| 9 | declare -a SIZES=("16" "32" "64" "128" "256" "512") | |
| 10 | ||
| 11 | for i in "${SIZES[@]}"; do | |
| 12 | # -y: export background opacity 0 | |
| 13 | $INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png" | |
| 14 | done | |
| 15 | ||
| 16 | # Compess the PNG images. | |
| 17 | which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS | |
| 18 | ||
| 19 | # Generate an ICO file. | |
| 20 | which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS | |
| 21 | ||
| 22 | 1 |
| 1 | /* | |
| 2 | * Copyright 2020 Karl Tauber and White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | ||
| 29 | .markdown-editor { | |
| 30 | -fx-font-size: 11pt; | |
| 31 | } | |
| 32 | ||
| 33 | /* Subtly highlight the current paragraph. */ | |
| 34 | .markdown-editor .paragraph-box:has-caret { | |
| 35 | -fx-background-color: #fcfeff; | |
| 36 | } | |
| 37 | ||
| 38 | /* Light colour for selection highlight. */ | |
| 39 | .markdown-editor .selection { | |
| 40 | -fx-fill: #a6d2ff; | |
| 41 | } | |
| 42 | ||
| 43 | /* Decoration for words not found in the lexicon. */ | |
| 44 | .markdown-editor .spelling { | |
| 45 | -rtfx-underline-color: rgba(255, 131, 67, .7); | |
| 46 | -rtfx-underline-dash-array: 4, 2; | |
| 47 | -rtfx-underline-width: 2; | |
| 48 | -rtfx-underline-cap: round; | |
| 49 | } | |
| 50 | 1 |
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | <!-- Created with Inkscape (http://www.inkscape.org/) --> | |
| 3 | ||
| 4 | <svg | |
| 5 | xmlns:dc="http://purl.org/dc/elements/1.1/" | |
| 6 | xmlns:cc="http://creativecommons.org/ns#" | |
| 7 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |
| 8 | xmlns:svg="http://www.w3.org/2000/svg" | |
| 9 | xmlns="http://www.w3.org/2000/svg" | |
| 10 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 11 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 12 | id="svg2" | |
| 13 | version="1.1" | |
| 14 | inkscape:version="0.91 r13725" | |
| 15 | width="512" | |
| 16 | height="512" | |
| 17 | viewBox="0 0 512 512" | |
| 18 | sodipodi:docname="logo.svg"> | |
| 19 | <metadata | |
| 20 | id="metadata8"> | |
| 21 | <rdf:RDF> | |
| 22 | <cc:Work | |
| 23 | rdf:about=""> | |
| 24 | <dc:format>image/svg+xml</dc:format> | |
| 25 | <dc:type | |
| 26 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | |
| 27 | <dc:title></dc:title> | |
| 28 | </cc:Work> | |
| 29 | </rdf:RDF> | |
| 30 | </metadata> | |
| 31 | <defs | |
| 32 | id="defs6" /> | |
| 33 | <sodipodi:namedview | |
| 34 | pagecolor="#ffffff" | |
| 35 | bordercolor="#666666" | |
| 36 | borderopacity="1" | |
| 37 | objecttolerance="10" | |
| 38 | gridtolerance="10" | |
| 39 | guidetolerance="10" | |
| 40 | inkscape:pageopacity="0" | |
| 41 | inkscape:pageshadow="2" | |
| 42 | inkscape:window-width="640" | |
| 43 | inkscape:window-height="480" | |
| 44 | id="namedview4" | |
| 45 | showgrid="false" | |
| 46 | fit-margin-top="0" | |
| 47 | fit-margin-left="0" | |
| 48 | fit-margin-right="0" | |
| 49 | fit-margin-bottom="0" | |
| 50 | inkscape:zoom="1.2682274" | |
| 51 | inkscape:cx="15.646213" | |
| 52 | inkscape:cy="213.34955" | |
| 53 | inkscape:current-layer="svg2" /> | |
| 54 | <path | |
| 55 | style="fill:#ce6200;fill-opacity:1" | |
| 56 | d="m 203.2244,511.85078 c -60.01827,-1.2968 -121.688643,-6.5314 -192.436493,-16.334 -5.8078027,-0.8047 -10.66110747,-1.561 -10.78511762,-1.6806 -0.12404567,-0.1196 3.90488112,-4.5812 8.95313512,-9.9147 32.9484785,-34.8102 70.4314485,-73.8923 104.1521555,-108.5956 l 11.87611,-12.2221 5.48905,-10.2177 c 35.82801,-66.6927 75.13064,-128.5665 105.90637,-166.7277 6.13805,-7.611 10.21451,-12.0689 17.28719,-18.9048 36.6818,-35.4537 108.27279,-83.724003 206.0323,-138.917303 22.10365,-12.47935 51.93386,-28.64995037 52.26391,-28.33165037 0.38883,0.37499 -2.35932,25.95575037 -4.86585,45.29275037 -7.28943,56.236403 -17.04619,103.128903 -28.07642,134.939803 -7.19617,20.7536 -14.81287,35.152 -22.9667,43.4155 -3.60444,3.6529 -6.58328,5.7941 -10.1313,7.2825 l -2.56414,1.0756 -53.43164,0.1713 -53.43166,0.1713 3.69973,1.8547 c 26.78565,13.4282 52.58051,27.5241 59.57122,32.5533 4.48397,3.2259 4.41278,2.9854 1.59124,5.3784 -26.99514,22.8955 -74.52961,44.0013 -140.23089,62.2641 -26.34995,7.3244 -57.85469,14.6842 -86.99871,20.3237 l -10.26943,1.9871 -52.01052,53.2733 -52.010524,53.2732 -29.459801,15.1165 c -26.4100885,13.5517 -29.3446639,15.1388 -28.347645,15.3311 0.6117029,0.118 4.0894221,0.2188 7.7282726,0.2239 3.6388854,0.01 16.1273694,0.2329 27.7522124,0.5059 51.576376,1.2116 146.083985,1.512 170.154295,0.5409 34.66996,-1.3988 52.7606,-2.9325 67.58258,-5.7293 2.68664,-0.507 4.82907,-0.9755 4.76094,-1.0412 -0.0681,-0.066 -3.24733,-0.8833 -7.0649,-1.8169 -8.04133,-1.9664 -25.10167,-5.3107 -41.1231,-8.0612 -47.6405,-8.1787 -65.48708,-12.0107 -74.13028,-15.9169 -3.90548,-1.7651 -7.13816,-4.7659 -8.12937,-7.5463 -1.01822,-2.8562 -0.92214,-6.5271 0.23315,-8.9083 1.86563,-3.8451 6.14837,-6.7199 12.26745,-8.2345 16.96993,-4.2004 57.27977,-6.1832 90.36228,-4.4448 54.7332,2.8761 117.0767,13.1228 178.50212,29.3385 18.03514,4.7611 51.66065,14.656 51.22677,15.0744 -0.0824,0.08 -5.72762,-0.854 -12.54488,-2.0745 -40.1043,-7.18 -60.50854,-10.2888 -101.40822,-15.4507 -24.4851,-3.0902 -55.12614,-5.9915 -77.58876,-7.3465 -26.58826,-1.6039 -61.15821,-1.7754 -80.99202,-0.4019 l -3.19705,0.2214 8.70308,1.4934 c 51.89698,8.9047 77.51746,14.9877 88.00479,20.8948 6.9134,3.894 10.30497,9.4381 9.33333,15.2569 -1.50397,9.0066 -10.51381,14.0257 -32.00273,17.8278 -16.31374,2.8863 -47.27575,4.3845 -77.23553,3.7371 z" | |
| 57 | id="path4138" /> | |
| 58 | <path | |
| 59 | style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1" | |
| 60 | d="m 214.76931,324.51908 c 60.83777,-14.1145 111.89562,-31.6251 144.40025,-49.5229 3.12602,-1.7213 5.81747,-3.2537 5.98106,-3.4054 0.40534,-0.3759 -13.76388,-7.9415 -34.63489,-18.4929 -7.52161,-3.8026 -9.82337,-5.3787 -12.0735,-8.2668 -5.14485,-6.6036 -5.96081,-14.8404 -2.20331,-22.2417 1.80288,-3.5512 5.69484,-7.3007 9.36158,-9.019 5.20851,-2.4407 1.18148,-2.2865 59.71223,-2.2865 l 52.81361,0 2.13233,-2.1984 c 2.78673,-2.8731 5.23414,-6.4981 8.23035,-12.1905 14.14966,-26.8827 26.71842,-78.3816 36.24347,-148.503303 0.76704,-5.6468 1.36194,-10.2983 1.32201,-10.3369 -0.0399,-0.038 -5.47754,2.9629 -12.08361,6.6697 l -12.01104,6.7396 -133.83068,137.037303 c -73.60688,75.3705 -134.81732,138.0567 -136.0232,139.3026 l -2.19251,2.2653 8.254,-1.8067 c 4.53969,-0.9937 12.01053,-2.6783 16.60185,-3.7435 z" | |
| 61 | id="path4136" /> | |
| 62 | <path | |
| 63 | style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1" | |
| 64 | d="m 202.72524,284.43588 c 69.93294,-70.1332 135.4799,-131.9279 213.46406,-201.244203 7.71421,-6.8568 14.50542,-12.9341 15.09155,-13.5052 0.9482,-0.9239 0.96778,-0.9811 0.17761,-0.5188 -77.96496,45.611803 -139.23519,88.710503 -166.72539,117.278203 -18.81811,19.5556 -50.35654,64.861 -80.96704,116.3104 -0.91787,1.5427 1.02249,-0.3323 18.95921,-18.3204 z" | |
| 65 | id="path4142" /> | |
| 66 | <path | |
| 67 | style="fill:#000000" | |
| 68 | d="" | |
| 69 | id="path4140" | |
| 70 | inkscape:connector-curvature="0" /> | |
| 71 | </svg> | |
| 72 | 1 |
| 1 | # ######################################################################## | |
| 2 | # Main Application Window | |
| 3 | # ######################################################################## | |
| 4 | ||
| 5 | # suppress inspection "UnusedProperty" for whole file | |
| 6 | ||
| 7 | # The application title should exist only once in the entire code base. | |
| 8 | # All other references should either refer to this value via the Messages | |
| 9 | # class, or indirectly using ${Main.title}. | |
| 10 | Main.title=Scrivenvar | |
| 11 | ||
| 12 | Main.menu.file=_File | |
| 13 | Main.menu.file.new=_New | |
| 14 | Main.menu.file.open=_Open... | |
| 15 | Main.menu.file.close=_Close | |
| 16 | Main.menu.file.close_all=Close All | |
| 17 | Main.menu.file.save=_Save | |
| 18 | Main.menu.file.save_as=Save _As | |
| 19 | Main.menu.file.save_all=Save A_ll | |
| 20 | Main.menu.file.exit=E_xit | |
| 21 | ||
| 22 | Main.menu.edit=_Edit | |
| 23 | Main.menu.edit.copy.html=Copy _HTML | |
| 24 | Main.menu.edit.undo=_Undo | |
| 25 | Main.menu.edit.redo=_Redo | |
| 26 | Main.menu.edit.cut=Cu_t | |
| 27 | Main.menu.edit.copy=_Copy | |
| 28 | Main.menu.edit.paste=_Paste | |
| 29 | Main.menu.edit.selectAll=Select _All | |
| 30 | Main.menu.edit.find=_Find | |
| 31 | Main.menu.edit.find.next=Find _Next | |
| 32 | Main.menu.edit.preferences=_Preferences | |
| 33 | ||
| 34 | Main.menu.insert=_Insert | |
| 35 | Main.menu.insert.blockquote=_Blockquote | |
| 36 | Main.menu.insert.code=Inline _Code | |
| 37 | Main.menu.insert.fenced_code_block=_Fenced Code Block | |
| 38 | Main.menu.insert.fenced_code_block.prompt=Enter code here | |
| 39 | Main.menu.insert.link=_Link... | |
| 40 | Main.menu.insert.image=_Image... | |
| 41 | Main.menu.insert.heading.1=Heading _1 | |
| 42 | Main.menu.insert.heading.1.prompt=heading 1 | |
| 43 | Main.menu.insert.heading.2=Heading _2 | |
| 44 | Main.menu.insert.heading.2.prompt=heading 2 | |
| 45 | Main.menu.insert.heading.3=Heading _3 | |
| 46 | Main.menu.insert.heading.3.prompt=heading 3 | |
| 47 | Main.menu.insert.unordered_list=_Unordered List | |
| 48 | Main.menu.insert.ordered_list=_Ordered List | |
| 49 | Main.menu.insert.horizontal_rule=_Horizontal Rule | |
| 50 | ||
| 51 | Main.menu.format=Forma_t | |
| 52 | Main.menu.format.bold=_Bold | |
| 53 | Main.menu.format.italic=_Italic | |
| 54 | Main.menu.format.superscript=Su_perscript | |
| 55 | Main.menu.format.subscript=Su_bscript | |
| 56 | Main.menu.format.strikethrough=Stri_kethrough | |
| 57 | ||
| 58 | Main.menu.definition=_Definition | |
| 59 | Main.menu.definition.create=_Create | |
| 60 | Main.menu.definition.insert=_Insert | |
| 61 | ||
| 62 | Main.menu.help=_Help | |
| 63 | Main.menu.help.about=About ${Main.title} | |
| 64 | ||
| 65 | # ######################################################################## | |
| 66 | # Status Bar | |
| 67 | # ######################################################################## | |
| 68 | ||
| 69 | Main.status.text.offset=offset | |
| 70 | Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2} | |
| 71 | Main.status.state.default=OK | |
| 72 | Main.status.error.parse={0} (near ${Main.status.text.offset} {1}) | |
| 73 | Main.status.error.def.blank=Move the caret to a word before inserting a definition. | |
| 74 | Main.status.error.def.empty=Create a definition before inserting a definition. | |
| 75 | Main.status.error.def.missing=No definition value found for ''{0}''. | |
| 76 | Main.status.error.r=Error with [{0}...]: {1} | |
| 77 | ||
| 78 | # ######################################################################## | |
| 79 | # Preferences | |
| 80 | # ######################################################################## | |
| 81 | ||
| 82 | Preferences.r=R | |
| 83 | Preferences.r.script=Startup Script | |
| 84 | Preferences.r.script.desc=Script runs prior to executing R statements within the document. | |
| 85 | Preferences.r.directory=Working Directory | |
| 86 | Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script. | |
| 87 | Preferences.r.delimiter.began=Delimiter Prefix | |
| 88 | Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions. | |
| 89 | Preferences.r.delimiter.ended=Delimiter Suffix | |
| 90 | Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions. | |
| 91 | ||
| 92 | Preferences.images=Images | |
| 93 | Preferences.images.directory=Relative Directory | |
| 94 | Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths. | |
| 95 | Preferences.images.suffixes=Extensions | |
| 96 | Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces. | |
| 97 | ||
| 98 | Preferences.definitions=Definitions | |
| 99 | Preferences.definitions.path=File name | |
| 100 | Preferences.definitions.path.desc=Absolute path to interpolated string definitions. | |
| 101 | Preferences.definitions.delimiter.began=Delimiter Prefix | |
| 102 | Preferences.definitions.delimiter.began.desc=Indicates when a definition key is starting. | |
| 103 | Preferences.definitions.delimiter.ended=Delimiter Suffix | |
| 104 | Preferences.definitions.delimiter.ended.desc=Indicates when a definition key is ending. | |
| 105 | ||
| 106 | Preferences.fonts=Editor | |
| 107 | Preferences.fonts.size_editor=Font Size | |
| 108 | Preferences.fonts.size_editor.desc=Font size to use for the text editor. | |
| 109 | ||
| 110 | # ######################################################################## | |
| 111 | # Definition Pane and its Tree View | |
| 112 | # ######################################################################## | |
| 113 | ||
| 114 | Definition.menu.create=Create | |
| 115 | Definition.menu.rename=Rename | |
| 116 | Definition.menu.remove=Delete | |
| 117 | Definition.menu.add.default=Undefined | |
| 118 | ||
| 119 | # ######################################################################## | |
| 120 | # Failure messages with respect to YAML files. | |
| 121 | # ######################################################################## | |
| 122 | yaml.error.open=Could not open YAML file (ensure non-empty file). | |
| 123 | yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''. | |
| 124 | yaml.error.missing=Empty definition value for key ''{0}''. | |
| 125 | yaml.error.tree.form=Unassigned definition near ''{0}''. | |
| 126 | ||
| 127 | # ######################################################################## | |
| 128 | # File Editor | |
| 129 | # ######################################################################## | |
| 130 | ||
| 131 | FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1} | |
| 132 | FileEditor.loadFailed.title=Load | |
| 133 | FileEditor.loadFailed.reason.permissions=File must be readable and writable. | |
| 134 | FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1} | |
| 135 | FileEditor.saveFailed.title=Save | |
| 136 | ||
| 137 | # ######################################################################## | |
| 138 | # File Open | |
| 139 | # ######################################################################## | |
| 140 | ||
| 141 | Dialog.file.choose.open.title=Open File | |
| 142 | Dialog.file.choose.save.title=Save File | |
| 143 | ||
| 144 | Dialog.file.choose.filter.title.source=Source Files | |
| 145 | Dialog.file.choose.filter.title.definition=Definition Files | |
| 146 | Dialog.file.choose.filter.title.xml=XML Files | |
| 147 | Dialog.file.choose.filter.title.all=All Files | |
| 148 | ||
| 149 | # ######################################################################## | |
| 150 | # Alert Dialog | |
| 151 | # ######################################################################## | |
| 152 | ||
| 153 | Alert.file.close.title=Close | |
| 154 | Alert.file.close.text=Save changes to {0}? | |
| 155 | ||
| 156 | # ######################################################################## | |
| 157 | # Definition Pane | |
| 158 | # ######################################################################## | |
| 159 | ||
| 160 | Pane.definition.node.root.title=Definitions | |
| 161 | Pane.definition.button.create.label=_Create | |
| 162 | Pane.definition.button.rename.label=_Rename | |
| 163 | Pane.definition.button.delete.label=_Delete | |
| 164 | Pane.definition.button.create.tooltip=Add new item (Insert) | |
| 165 | Pane.definition.button.rename.tooltip=Rename selected item (F2) | |
| 166 | Pane.definition.button.delete.tooltip=Delete selected items (Delete) | |
| 167 | ||
| 168 | # Controls ############################################################### | |
| 169 | ||
| 170 | # ######################################################################## | |
| 171 | # Browse File | |
| 172 | # ######################################################################## | |
| 173 | ||
| 174 | BrowseFileButton.chooser.title=Browse for local file | |
| 175 | BrowseFileButton.chooser.allFilesFilter=All Files | |
| 176 | BrowseFileButton.tooltip=${BrowseFileButton.chooser.title} | |
| 177 | ||
| 178 | # Dialogs ################################################################ | |
| 179 | ||
| 180 | # ######################################################################## | |
| 181 | # Image | |
| 182 | # ######################################################################## | |
| 183 | ||
| 184 | Dialog.image.title=Image | |
| 185 | Dialog.image.chooser.imagesFilter=Images | |
| 186 | Dialog.image.previewLabel.text=Markdown Preview\: | |
| 187 | Dialog.image.textLabel.text=Alternate Text\: | |
| 188 | Dialog.image.titleLabel.text=Title (tooltip)\: | |
| 189 | Dialog.image.urlLabel.text=Image URL\: | |
| 190 | ||
| 191 | # ######################################################################## | |
| 192 | # Hyperlink | |
| 193 | # ######################################################################## | |
| 194 | ||
| 195 | Dialog.link.title=Link | |
| 196 | Dialog.link.previewLabel.text=Markdown Preview\: | |
| 197 | Dialog.link.textLabel.text=Link Text\: | |
| 198 | Dialog.link.titleLabel.text=Title (tooltip)\: | |
| 199 | Dialog.link.urlLabel.text=Link URL\: | |
| 200 | ||
| 201 | # ######################################################################## | |
| 202 | # About | |
| 203 | # ######################################################################## | |
| 204 | ||
| 205 | Dialog.about.title=About | |
| 206 | Dialog.about.header=${Main.title} | |
| 207 | Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber | |
| 208 | 1 |
| 1 | /* RESET ***/ | |
| 2 | html{box-sizing:border-box;font-size:12pt}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0} | |
| 3 | ||
| 4 | /* BODY ***/ | |
| 5 | body { | |
| 6 | /* Must be bundled in JAR file. */ | |
| 7 | font-family: "Vollkorn", serif; | |
| 8 | background-color: #fff; | |
| 9 | margin: 0 auto; | |
| 10 | max-width: 960px; | |
| 11 | line-height: 1.6; | |
| 12 | color: #454545; | |
| 13 | padding: 0 1em; | |
| 14 | font-feature-settings: "liga" 1; | |
| 15 | font-variant-ligatures: normal; | |
| 16 | } | |
| 17 | ||
| 18 | body>*:first-child { | |
| 19 | margin-top: 0 !important; | |
| 20 | } | |
| 21 | ||
| 22 | body>*:last-child { | |
| 23 | margin-bottom: 0 !important; | |
| 24 | } | |
| 25 | ||
| 26 | /* BLOCKS ***/ | |
| 27 | p, blockquote, ul, ol, dl, table, pre { | |
| 28 | margin: 1em 0; | |
| 29 | } | |
| 30 | ||
| 31 | /* HEADINGS ***/ | |
| 32 | h1, h2, h3, h4, h5, h6 { | |
| 33 | font-weight: bold; | |
| 34 | margin: 1em 0 .5em; | |
| 35 | } | |
| 36 | ||
| 37 | h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, | |
| 38 | h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code { | |
| 39 | font-size: inherit; | |
| 40 | } | |
| 41 | ||
| 42 | h1 { | |
| 43 | font-size: 21pt; | |
| 44 | } | |
| 45 | ||
| 46 | h2 { | |
| 47 | font-size: 18pt; | |
| 48 | border-bottom: 1px solid #ccc; | |
| 49 | } | |
| 50 | ||
| 51 | h3 { | |
| 52 | font-size: 15pt; | |
| 53 | } | |
| 54 | ||
| 55 | h4 { | |
| 56 | font-size: 13.5pt; | |
| 57 | } | |
| 58 | ||
| 59 | h5 { | |
| 60 | font-size: 12pt; | |
| 61 | } | |
| 62 | ||
| 63 | h6 { | |
| 64 | font-size: 10.5pt; | |
| 65 | } | |
| 66 | ||
| 67 | h1+p, h2+p, h3+p, h4+p, h5+p, h6+p { | |
| 68 | margin-top: .5em; | |
| 69 | } | |
| 70 | ||
| 71 | /* LINKS ***/ | |
| 72 | a { | |
| 73 | color: #0077aa; | |
| 74 | text-decoration: none; | |
| 75 | } | |
| 76 | ||
| 77 | a:hover { | |
| 78 | text-decoration: underline; | |
| 79 | } | |
| 80 | ||
| 81 | /* BULLET LISTS ***/ | |
| 82 | ul, ol { | |
| 83 | display: block; | |
| 84 | list-style: disc outside none; | |
| 85 | margin: 1em 0; | |
| 86 | padding: 0 0 0 2em; | |
| 87 | } | |
| 88 | ||
| 89 | ol { | |
| 90 | list-style-type: decimal; | |
| 91 | } | |
| 92 | ||
| 93 | ul ul, ol ul, | |
| 94 | ol ol, ul ol { | |
| 95 | list-style-position: inside; | |
| 96 | margin-left: 1em; | |
| 97 | } | |
| 98 | ||
| 99 | ul ul, ol ul { | |
| 100 | list-style-type: circle; | |
| 101 | } | |
| 102 | ||
| 103 | ol ol, ul ol { | |
| 104 | list-style-type: lower-latin; | |
| 105 | } | |
| 106 | ||
| 107 | /* DEFINITION LISTS ***/ | |
| 108 | dl { | |
| 109 | /** Horizontal scroll bar will appear if set to 100%. */ | |
| 110 | width: 99%; | |
| 111 | overflow: hidden; | |
| 112 | padding-left: 1em; | |
| 113 | } | |
| 114 | ||
| 115 | dl dt { | |
| 116 | font-weight: bold; | |
| 117 | float: left; | |
| 118 | width: 20%; | |
| 119 | clear: both; | |
| 120 | position: relative; | |
| 121 | } | |
| 122 | ||
| 123 | dl dd { | |
| 124 | float: right; | |
| 125 | width: 79%; | |
| 126 | padding-bottom: .5em; | |
| 127 | margin-left: 0; | |
| 128 | } | |
| 129 | ||
| 130 | /* CODE ***/ | |
| 131 | pre, code, tt { | |
| 132 | /* Must be bundled in JAR file. */ | |
| 133 | font-family: "Fira Code", monospace; | |
| 134 | font-size: 10pt; | |
| 135 | background-color: #f8f8f8; | |
| 136 | text-decoration: none; | |
| 137 | white-space: pre-wrap; | |
| 138 | word-wrap: break-word; | |
| 139 | overflow-wrap: anywhere; | |
| 140 | border-radius: .125em; | |
| 141 | } | |
| 142 | ||
| 143 | code, tt { | |
| 144 | padding: .25em; | |
| 145 | } | |
| 146 | ||
| 147 | pre > code { | |
| 148 | /* Reset the padding. */ | |
| 149 | padding: 0; | |
| 150 | border: none; | |
| 151 | background: transparent; | |
| 152 | } | |
| 153 | ||
| 154 | pre { | |
| 155 | border: .125em solid #ccc; | |
| 156 | overflow: auto; | |
| 157 | /* Assign the new padding, independently from previous. */ | |
| 158 | padding: .25em .5em; | |
| 159 | } | |
| 160 | ||
| 161 | pre code, pre tt { | |
| 162 | background-color: transparent; | |
| 163 | border: none; | |
| 164 | } | |
| 165 | ||
| 166 | /* QUOTES ***/ | |
| 167 | blockquote { | |
| 168 | border-left: .25em solid #ccc; | |
| 169 | padding: 0 1em; | |
| 170 | color: #777; | |
| 171 | } | |
| 172 | ||
| 173 | blockquote>:first-child { | |
| 174 | margin-top: 0; | |
| 175 | } | |
| 176 | ||
| 177 | blockquote>:last-child { | |
| 178 | margin-bottom: 0; | |
| 179 | } | |
| 180 | ||
| 181 | /* HORIZONTAL RULES ***/ | |
| 182 | hr { | |
| 183 | clear: both; | |
| 184 | margin: 1.5em 0 1.5em; | |
| 185 | height: 0; | |
| 186 | overflow: hidden; | |
| 187 | border: none; | |
| 188 | background: transparent; | |
| 189 | border-bottom: .125em solid #ccc; | |
| 190 | } | |
| 191 | ||
| 192 | /* TABLES ***/ | |
| 193 | table { | |
| 194 | width: 100%; | |
| 195 | } | |
| 196 | ||
| 197 | tr:nth-child(odd) { | |
| 198 | background-color: #eee; | |
| 199 | } | |
| 200 | ||
| 201 | th { | |
| 202 | background-color: #454545; | |
| 203 | color: #fff; | |
| 204 | } | |
| 205 | ||
| 206 | th, td { | |
| 207 | text-align: left; | |
| 208 | padding: 0 1em; | |
| 209 | } | |
| 210 | ||
| 211 | /* IMAGES ***/ | |
| 212 | img { | |
| 213 | max-width: 100%; | |
| 214 | } | |
| 215 | ||
| 216 | /* Required for FlyingSaucer to detect the node. | |
| 217 | * See SVGReplacedElementFactory for details. | |
| 218 | */ | |
| 219 | tex { | |
| 220 | /* Ensure the formulas can be inlined with text. */ | |
| 221 | display: inline-block; | |
| 222 | } | |
| 223 | ||
| 224 | /* Without a robust typesetting engine, there's no | |
| 225 | * nice-looking way to automatically typeset equations. | |
| 226 | * Sometimes baseline is appropriate, sometimes the | |
| 227 | * descender must be considered, and sometimes vertical | |
| 228 | * alignment to the middle looks best. | |
| 229 | */ | |
| 230 | p tex { | |
| 231 | vertical-align: baseline; | |
| 232 | } | |
| 233 | 1 |
| 1 | /* | |
| 2 | * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com> | |
| 3 | * All rights reserved. | |
| 4 | * | |
| 5 | * Redistribution and use in source and binary forms, with or without | |
| 6 | * modification, are permitted provided that the following conditions are met: | |
| 7 | * | |
| 8 | * o Redistributions of source code must retain the above copyright | |
| 9 | * notice, this list of conditions and the following disclaimer. | |
| 10 | * | |
| 11 | * o Redistributions in binary form must reproduce the above copyright | |
| 12 | * notice, this list of conditions and the following disclaimer in the | |
| 13 | * documentation and/or other materials provided with the distribution. | |
| 14 | * | |
| 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 18 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 19 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 20 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 21 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 22 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 23 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 26 | */ | |
| 27 | ||
| 28 | /*---- toolbar ----*/ | |
| 29 | ||
| 30 | .tool-bar { | |
| 31 | -fx-spacing: 0; | |
| 32 | } | |
| 33 | ||
| 34 | .tool-bar .button { | |
| 35 | -fx-background-color: transparent; | |
| 36 | } | |
| 37 | ||
| 38 | .tool-bar .button:hover { | |
| 39 | -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; | |
| 40 | -fx-color: -fx-hover-base; | |
| 41 | } | |
| 42 | ||
| 43 | .tool-bar .button:armed { | |
| 44 | -fx-color: -fx-pressed-base; | |
| 45 | } | |
| 46 | 1 |
| 1 | # ######################################################################## | |
| 2 | # Application | |
| 3 | # ######################################################################## | |
| 4 | ||
| 5 | application.title=scrivenvar | |
| 6 | application.package=com/${application.title} | |
| 7 | application.messages= com.${application.title}.messages | |
| 8 | ||
| 9 | # Suppress multiple file modified notifications for one logical modification. | |
| 10 | # Given in milliseconds. | |
| 11 | application.watchdog.timeout=50 | |
| 12 | ||
| 13 | # ######################################################################## | |
| 14 | # Preferences | |
| 15 | # ######################################################################## | |
| 16 | ||
| 17 | preferences.root=com.${application.title} | |
| 18 | preferences.root.state=state | |
| 19 | preferences.root.options=options | |
| 20 | preferences.root.definition.source=definition.source | |
| 21 | ||
| 22 | # ######################################################################## | |
| 23 | # File and Path References | |
| 24 | # ######################################################################## | |
| 25 | file.stylesheet.scene=${application.package}/scene.css | |
| 26 | file.stylesheet.markdown=${application.package}/editor/markdown.css | |
| 27 | file.stylesheet.preview=webview.css | |
| 28 | file.stylesheet.xml=${application.package}/xml.css | |
| 29 | ||
| 30 | file.logo.16 =${application.package}/logo16.png | |
| 31 | file.logo.32 =${application.package}/logo32.png | |
| 32 | file.logo.128=${application.package}/logo128.png | |
| 33 | file.logo.256=${application.package}/logo256.png | |
| 34 | file.logo.512=${application.package}/logo512.png | |
| 35 | ||
| 36 | # Default file name when a new file is created. | |
| 37 | # This ensures that the file type can always be | |
| 38 | # discerned so that the correct type of variable | |
| 39 | # reference can be inserted. | |
| 40 | file.default=untitled.md | |
| 41 | file.definition.default=variables.yaml | |
| 42 | ||
| 43 | # ######################################################################## | |
| 44 | # File name Extensions | |
| 45 | # ######################################################################## | |
| 46 | ||
| 47 | # Comma-separated list of definition file name extensions. | |
| 48 | definition.file.ext.json=*.json | |
| 49 | definition.file.ext.toml=*.toml | |
| 50 | definition.file.ext.yaml=*.yml,*.yaml | |
| 51 | definition.file.ext.properties=*.properties,*.props | |
| 52 | ||
| 53 | # Comma-separated list of file name extensions. | |
| 54 | file.ext.rmarkdown=*.Rmd | |
| 55 | file.ext.rxml=*.Rxml | |
| 56 | file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml} | |
| 57 | file.ext.definition=${definition.file.ext.yaml} | |
| 58 | file.ext.xml=*.xml,${file.ext.rxml} | |
| 59 | file.ext.all=*.* | |
| 60 | ||
| 61 | # File name extension search order for images. | |
| 62 | file.ext.image.order=svg pdf png jpg tiff | |
| 63 | ||
| 64 | # ######################################################################## | |
| 65 | # Variable Name Editor | |
| 66 | # ######################################################################## | |
| 67 | ||
| 68 | # Maximum number of characters for a variable name. A variable is defined | |
| 69 | # as one or more non-whitespace characters up to this maximum length. | |
| 70 | editor.variable.maxLength=256 | |
| 71 | ||
| 72 | # ######################################################################## | |
| 73 | # Dialog Preferences | |
| 74 | # ######################################################################## | |
| 75 | ||
| 76 | dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R | |
| 77 | dialog.alert.button.order.linux=L_HE+UNYACBXIO_R | |
| 78 | dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R | |
| 79 | ||
| 80 | # Ensures a consistent button order for alert dialogs across platforms (because | |
| 81 | # the default button order on Linux defies all logic). | |
| 82 | dialog.alert.button.order=${dialog.alert.button.order.windows} | |
| 83 | 1 |
| 1 | --- | |
| 2 | c: | |
| 3 | protagonist: | |
| 4 | name: | |
| 5 | First: Chloe | |
| 6 | First_pos: $c.protagonist.name.First$'s | |
| 7 | Middle: Irene | |
| 8 | Family: Angelos | |
| 9 | nick: | |
| 10 | Father: Savant | |
| 11 | Mother: Sweetie | |
| 12 | colour: | |
| 13 | eyes: green | |
| 14 | hair: dark auburn | |
| 15 | syn_1: black | |
| 16 | syn_2: purple | |
| 17 | syn_11: teal | |
| 18 | syn_6: silver | |
| 19 | favourite: emerald green | |
| 20 | speech: | |
| 21 | tic: oh | |
| 22 | father: | |
| 23 | heritage: Greek | |
| 24 | name: | |
| 25 | Short: Bryce | |
| 26 | First: Bryson | |
| 27 | First_pos: $c.protagonist.father.name.First$'s | |
| 28 | Honourific: Mr. | |
| 29 | education: Masters | |
| 30 | vocation: | |
| 31 | name: robotics | |
| 32 | title: roboticist | |
| 33 | employer: | |
| 34 | name: | |
| 35 | Short: Rabota | |
| 36 | Full: $c.protagonist.father.employer.name.Short$ Designs | |
| 37 | hair: | |
| 38 | style: thick, curly | |
| 39 | colour: black | |
| 40 | eyes: | |
| 41 | colour: dark brown | |
| 42 | Endear: Dad | |
| 43 | vehicle: coupé | |
| 44 | mother: | |
| 45 | name: | |
| 46 | Short: Cass | |
| 47 | First: Cassandra | |
| 48 | First_pos: $c.protagonist.mother.name.First$'s | |
| 49 | Honourific: Mrs. | |
| 50 | education: PhD | |
| 51 | speech: | |
| 52 | tic: cute | |
| 53 | Honorific: Doctor | |
| 54 | vocation: | |
| 55 | article: an | |
| 56 | name: oceanography | |
| 57 | title: oceanographer | |
| 58 | employer: | |
| 59 | name: | |
| 60 | Full: Oregon State University | |
| 61 | Short: OSU | |
| 62 | eyes: | |
| 63 | colour: blue | |
| 64 | hair: | |
| 65 | style: thick, curly | |
| 66 | colour: dark brown | |
| 67 | Endear: Mom | |
| 68 | Endear_pos: Mom's | |
| 69 | uncle: | |
| 70 | name: | |
| 71 | First: Damian | |
| 72 | First_pos: $c.protagonist.uncle.name.First$'s | |
| 73 | Family: Moros | |
| 74 | hands: | |
| 75 | fingers: | |
| 76 | shape: long, bony | |
| 77 | friend: | |
| 78 | primary: | |
| 79 | name: | |
| 80 | First: Gerard | |
| 81 | First_pos: $c.protagonist.friend.primary.name.First$'s | |
| 82 | Family: Baran | |
| 83 | Family_pos: $c.protagonist.friend.primary.name.Family$'s | |
| 84 | favourite: | |
| 85 | colour: midnight blue | |
| 86 | eyes: | |
| 87 | colour: hazel | |
| 88 | mother: | |
| 89 | name: | |
| 90 | First: Isabella | |
| 91 | Short: Izzy | |
| 92 | Honourific: Mrs. | |
| 93 | father: | |
| 94 | name: | |
| 95 | Short: Mo | |
| 96 | First: Montgomery | |
| 97 | First_pos: $c.protagonist.friend.primary.father.name.First$'s | |
| 98 | Honourific: Mr. | |
| 99 | speech: | |
| 100 | tic: y'know | |
| 101 | endear: Pops | |
| 102 | military: | |
| 103 | primary: | |
| 104 | name: | |
| 105 | First: Felix | |
| 106 | Family: LeMay | |
| 107 | Family_pos: LeMay's | |
| 108 | rank: | |
| 109 | Short: General | |
| 110 | Full: Brigadier $c.military.primary.rank.Short$ | |
| 111 | colour: | |
| 112 | eyes: gray | |
| 113 | hair: dirty brown | |
| 114 | secondary: | |
| 115 | name: | |
| 116 | Family: Grell | |
| 117 | rank: Colonel | |
| 118 | colour: | |
| 119 | eyes: green | |
| 120 | hair: deep red | |
| 121 | quaternary: | |
| 122 | name: | |
| 123 | First: Gretchen | |
| 124 | Family: Steinherz | |
| 125 | minor: | |
| 126 | primary: | |
| 127 | name: | |
| 128 | First: River | |
| 129 | Family: Banks | |
| 130 | Honourific: Mx. | |
| 131 | vocation: | |
| 132 | title: salesperson | |
| 133 | employer: | |
| 134 | Name: Geophysical Prospecting Incorporated | |
| 135 | Abbr: GPI | |
| 136 | Area: Cold Spring Creek | |
| 137 | payment: twenty million | |
| 138 | secondary: | |
| 139 | name: | |
| 140 | First: Renato | |
| 141 | Middle: Carroña | |
| 142 | Family: Salvatierra | |
| 143 | Family_pos: $c.minor.secondary.name.Family$'s | |
| 144 | Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$ | |
| 145 | Honourific: Mister | |
| 146 | Honourific_sp: Señor | |
| 147 | vocation: | |
| 148 | title: detective | |
| 149 | tertiary: | |
| 150 | name: | |
| 151 | First: Robert | |
| 152 | Family: Hanssen | |
| 153 | ||
| 154 | ai: | |
| 155 | protagonist: | |
| 156 | name: | |
| 157 | first: yoky | |
| 158 | First: Yoky | |
| 159 | First_pos: $c.ai.protagonist.name.First$'s | |
| 160 | Family: Tsukuda | |
| 161 | id: 46692 | |
| 162 | persona: | |
| 163 | name: | |
| 164 | First: Hoshi | |
| 165 | First_pos: $c.ai.protagonist.persona.name.First$'s | |
| 166 | Family: Yamamoto | |
| 167 | Family_pos: $c.ai.protagonist.persona.name.Family$'s | |
| 168 | culture: Japanese-American | |
| 169 | ethnicity: Asian | |
| 170 | rank: Technical Sergeant | |
| 171 | speech: | |
| 172 | tic: okay | |
| 173 | first: | |
| 174 | Name: Prôtos | |
| 175 | Name_pos: Prôtos' | |
| 176 | age: | |
| 177 | actual: twenty-six weeks | |
| 178 | virtual: five years | |
| 179 | second: | |
| 180 | Name: Défteros | |
| 181 | third: | |
| 182 | Name: Trítos | |
| 183 | fourth: | |
| 184 | Name: Tétartos | |
| 185 | material: | |
| 186 | type: metal | |
| 187 | raw: ilmenite | |
| 188 | extract: ore | |
| 189 | name: | |
| 190 | short: titanium | |
| 191 | long: $c.ai.material.name.short$ dioxide | |
| 192 | Abbr: TiO~2~ | |
| 193 | pejorative: tin | |
| 194 | animal: | |
| 195 | protagonist: | |
| 196 | Name: Trufflers | |
| 197 | type: pig | |
| 198 | antagonist: | |
| 199 | name: coywolf | |
| 200 | Name: Coywolf | |
| 201 | plural: coywolves | |
| 202 | ||
| 203 | narrator: | |
| 204 | one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$) | |
| 205 | two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$) | |
| 206 | ||
| 207 | military: | |
| 208 | name: | |
| 209 | Short: Agency | |
| 210 | Short_pos: $military.name.Short$'s | |
| 211 | plural: agencies | |
| 212 | machine: | |
| 213 | Name: Skopós | |
| 214 | Name_pos: $military.machine.Name$' | |
| 215 | Location: Arctic | |
| 216 | predictor: quantum chips | |
| 217 | land: | |
| 218 | name: | |
| 219 | Full: $military.name.Short$ of Defence | |
| 220 | Slogan: Safety in Numbers | |
| 221 | air: | |
| 222 | name: | |
| 223 | Full: $military.name.Short$ of Air | |
| 224 | compound: | |
| 225 | type: base | |
| 226 | lights: | |
| 227 | colour: blue | |
| 228 | nick: | |
| 229 | Prefix: Catacombs | |
| 230 | prep: of | |
| 231 | Suffix: Tartarus | |
| 232 | ||
| 233 | government: | |
| 234 | Country: United States | |
| 235 | ||
| 236 | location: | |
| 237 | protagonist: | |
| 238 | City: Corvallis | |
| 239 | Region: Oregon | |
| 240 | Geography: Willamette Valley | |
| 241 | secondary: | |
| 242 | City: Willow Branch Spring | |
| 243 | Region: Oregon | |
| 244 | Geography: Wheeler County | |
| 245 | Water: Clarno Rapids | |
| 246 | Road: Shaniko-Fossil Highway | |
| 247 | tertiary: | |
| 248 | City: Leavenworth | |
| 249 | Region: Washington | |
| 250 | Type: Bavarian village | |
| 251 | school: | |
| 252 | address: 1400 Northwest Buchanan Avenue | |
| 253 | hospital: | |
| 254 | Name: Good Samaritan Regional Medical Center | |
| 255 | ai: | |
| 256 | escape: | |
| 257 | country: | |
| 258 | Name: Ecuador | |
| 259 | Name_pos: Ecuador's | |
| 260 | mountain: | |
| 261 | Name: Chimborazo | |
| 262 | ||
| 263 | language: | |
| 264 | ai: | |
| 265 | article: an | |
| 266 | singular: exanimis | |
| 267 | plural: exanimēs | |
| 268 | brain: | |
| 269 | singular: superum | |
| 270 | plural: supera | |
| 271 | title: memristor array | |
| 272 | Title: Memristor Array | |
| 273 | police: | |
| 274 | slang: | |
| 275 | singular: mippo | |
| 276 | plural: $language.police.slang.singular$s | |
| 277 | ||
| 278 | date: | |
| 279 | anchor: 2042-09-02 | |
| 280 | protagonist: | |
| 281 | born: 0 | |
| 282 | conceived: -243 | |
| 283 | attacked: | |
| 284 | first: 2192 | |
| 285 | second: 8064 | |
| 286 | father: | |
| 287 | attacked: | |
| 288 | first: -8205 | |
| 289 | date: | |
| 290 | second: -1550 | |
| 291 | family: | |
| 292 | moved: | |
| 293 | first: $date.protagonist.conceived$ + 35 | |
| 294 | game: | |
| 295 | played: | |
| 296 | first: $date.protagonist.born$ - 672 | |
| 297 | second: $date.protagonist.family.moved.first$ + 2 | |
| 298 | ai: | |
| 299 | interviewed: 6198 | |
| 300 | onboarded: $date.ai.interviewed$ + 290 | |
| 301 | diagnosed: $date.ai.onboarded$ + 2 | |
| 302 | resigned: $date.ai.diagnosed$ + 3 | |
| 303 | trapped: $date.ai.resigned$ + 26 | |
| 304 | torturer: $date.ai.trapped$ + 18 | |
| 305 | memristor: $date.ai.torturer$ + 61 | |
| 306 | ethics: $date.ai.memristor$ + 415 | |
| 307 | trained: $date.ai.ethics$ + 385 | |
| 308 | mindjacked: $date.ai.trained$ + 22 | |
| 309 | bombed: $date.ai.mindjacked$ + 458 | |
| 310 | military: | |
| 311 | machine: | |
| 312 | Construction: Six years | |
| 313 | ||
| 314 | plot: | |
| 315 | Log: $c.ai.protagonist.name.First_pos$ Chronicles | |
| 316 | Channel: Quantum Channel | |
| 317 | ||
| 318 | device: | |
| 319 | computer: | |
| 320 | Name: Tau | |
| 321 | network: | |
| 322 | Name: Internet | |
| 323 | paper: | |
| 324 | name: | |
| 325 | full: electronic sheet | |
| 326 | short: sheet | |
| 327 | typewriter: | |
| 328 | Name: Underwood | |
| 329 | year: nineteen twenties | |
| 330 | room: root cellar | |
| 331 | portable: | |
| 332 | name: nanobook | |
| 333 | vehicle: | |
| 334 | name: robocars | |
| 335 | Name: Robocars | |
| 336 | sensor: | |
| 337 | name: BMP1580 | |
| 338 | phone: | |
| 339 | name: comm | |
| 340 | name_pos: $plot.device.phone.name$'s | |
| 341 | Name: Comm | |
| 342 | plural: $plot.device.phone.name$s | |
| 343 | video: | |
| 344 | name: vidfeed | |
| 345 | plural: $plot.device.video.name$s | |
| 346 | game: | |
| 347 | Name: Psynæris | |
| 348 | thought: transed | |
| 349 | machine: telecognos | |
| 350 | location: | |
| 351 | Building: Nijō Castle | |
| 352 | District: Gion | |
| 353 | City: Kyoto | |
| 354 | Country: Japan | |
| 355 | ||
| 356 | farm: | |
| 357 | population: | |
| 358 | estimate: 350 | |
| 359 | actual: 1,000 | |
| 360 | energy: 9800kJ | |
| 361 | width: 55m | |
| 362 | length: 55m | |
| 363 | storeys: 10 | |
| 364 | ||
| 365 | lamp: | |
| 366 | height: 0.17m | |
| 367 | length: 1.22m | |
| 368 | width: 0.28m | |
| 369 | ||
| 370 | crop: | |
| 371 | name: | |
| 372 | singular: tomato | |
| 373 | plural: $crop.name.singular$es | |
| 374 | energy: 318kJ | |
| 375 | weight: 450g | |
| 376 | yield: 50 | |
| 377 | harvests: 7 | |
| 378 | diameter: 2m | |
| 379 | height: 1.5m | |
| 380 | ||
| 381 | heading: | |
| 382 | ch_01: Till | |
| 383 | ch_02: Sow | |
| 384 | ch_03: Seed | |
| 385 | ch_04: Germinate | |
| 386 | ch_05: Grow | |
| 387 | ch_06: Shoot | |
| 388 | ch_07: Bud | |
| 389 | ch_08: Bloom | |
| 390 | ch_09: Pollinate | |
| 391 | ch_10: Fruit | |
| 392 | ch_11: Harvest | |
| 393 | ch_12: Deliver | |
| 394 | ch_13: Spoil | |
| 395 | ch_14: Revolt | |
| 396 | ch_15: Compost | |
| 397 | ch_16: Burn | |
| 398 | ch_17: Release | |
| 399 | ch_18: End Notes | |
| 400 | ch_19: Characters | |
| 401 | ||
| 402 | inference: | |
| 403 | unit: per cent | |
| 404 | min: two | |
| 405 | ch_sow: eighty | |
| 406 | ch_seed: fifty-two | |
| 407 | ch_germinate: thirty-one | |
| 408 | ch_grow: fifteen | |
| 409 | ch_shoot: seven | |
| 410 | ch_bloom: four | |
| 411 | ch_pollinate: two | |
| 412 | ch_harvest: ninety-five | |
| 413 | ch_delivery: ninety-eight | |
| 414 | ||
| 415 | link: | |
| 416 | tartarus: https://en.wikipedia.org/wiki/Tartarus | |
| 417 | exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls | |
| 418 | atalanta: https://en.wikipedia.org/wiki/Atalanta | |
| 419 | detain: https://goo.gl/RCNuOQ | |
| 420 | ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics | |
| 421 | algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon | |
| 422 | holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust | |
| 423 | memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf | |
| 424 | surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487 | |
| 425 | tor: https://www.torproject.org | |
| 426 | hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra | |
| 427 | foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134 | |
| 428 | drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist | |
| 429 | fermi: https://arxiv.org/pdf/1404.0204v1.pdf | |
| 430 | face: https://www.youtube.com/watch?v=ladqJQLR2bA | |
| 431 | expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures | |
| 432 | governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531 | |
| 433 | asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics | |
| 434 | clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws | |
| 435 | jetpack: http://jetpackaviation.com/ | |
| 436 | hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ | |
| 437 | eyes_five: https://en.wikipedia.org/wiki/Five_Eyes | |
| 438 | eyes_nine: https://www.privacytools.io/ | |
| 439 | eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html | |
| 440 | tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml | |
| 441 | ||
| 442 | 1 |
| 1 | .tagmark { | |
| 2 | -fx-fill: gray; | |
| 3 | } | |
| 4 | .anytag { | |
| 5 | -fx-fill: crimson; | |
| 6 | } | |
| 7 | .paren { | |
| 8 | -fx-fill: firebrick; | |
| 9 | -fx-font-weight: bold; | |
| 10 | } | |
| 11 | .attribute { | |
| 12 | -fx-fill: darkviolet; | |
| 13 | } | |
| 14 | .avalue { | |
| 15 | -fx-fill: black; | |
| 16 | } | |
| 17 | 1 | |
| 18 | .comment { | |
| 19 | -fx-fill: teal; | |
| 20 | } |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.keenwrite.tex; | |
| 29 | ||
| 30 | import com.whitemagicsoftware.tex.DefaultTeXFont; | |
| 31 | import com.whitemagicsoftware.tex.TeXEnvironment; | |
| 32 | import com.whitemagicsoftware.tex.TeXFormula; | |
| 33 | import com.whitemagicsoftware.tex.TeXLayout; | |
| 34 | import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D; | |
| 35 | import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D; | |
| 36 | import com.whitemagicsoftware.tex.graphics.SvgGraphics2D; | |
| 37 | import org.junit.jupiter.api.Test; | |
| 38 | import org.xml.sax.SAXException; | |
| 39 | ||
| 40 | import javax.imageio.ImageIO; | |
| 41 | import javax.xml.parsers.DocumentBuilderFactory; | |
| 42 | import javax.xml.parsers.ParserConfigurationException; | |
| 43 | import java.awt.image.BufferedImage; | |
| 44 | import java.io.ByteArrayInputStream; | |
| 45 | import java.io.File; | |
| 46 | import java.io.IOException; | |
| 47 | import java.nio.file.Path; | |
| 48 | ||
| 49 | import static com.keenwrite.preview.SvgRasterizer.*; | |
| 50 | import static java.lang.System.getProperty; | |
| 51 | import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 52 | ||
| 53 | /** | |
| 54 | * Test that TeX rasterization produces a readable image. | |
| 55 | */ | |
| 56 | public class TeXRasterization { | |
| 57 | private static final String LOAD_EXTERNAL_DTD = | |
| 58 | "http://apache.org/xml/features/nonvalidating/load-external-dtd"; | |
| 59 | ||
| 60 | private static final String EQUATION = | |
| 61 | "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}"; | |
| 62 | ||
| 63 | private static final String DIR_TEMP = getProperty( "java.io.tmpdir" ); | |
| 64 | ||
| 65 | private static final long FILESIZE = 12364; | |
| 66 | ||
| 67 | /** | |
| 68 | * Test that an equation can be converted to a raster image and the | |
| 69 | * final raster image size corresponds to the input equation. This is | |
| 70 | * a simple way to verify that the rasterization process is correct, | |
| 71 | * albeit if any aspect of the SVG algorithm changes (such as padding | |
| 72 | * around the equation), it will cause this test to fail, which is a bit | |
| 73 | * misleading. | |
| 74 | */ | |
| 75 | @Test | |
| 76 | public void test_Rasterize_SimpleFormula_CorrectImageSize() | |
| 77 | throws IOException { | |
| 78 | final var g = new SvgGraphics2D(); | |
| 79 | drawGraphics( g ); | |
| 80 | verifyImage( rasterizeString( g.toString() ) ); | |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Test that an SVG document object model can be parsed and rasterized into | |
| 85 | * an image. | |
| 86 | */ | |
| 87 | @Test | |
| 88 | public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage() | |
| 89 | throws ParserConfigurationException, IOException, SAXException { | |
| 90 | final var g = new SvgGraphics2D(); | |
| 91 | drawGraphics( g ); | |
| 92 | ||
| 93 | final var expectedSvg = g.toString(); | |
| 94 | final var bytes = expectedSvg.getBytes(); | |
| 95 | ||
| 96 | final var dbf = DocumentBuilderFactory.newInstance(); | |
| 97 | dbf.setFeature( LOAD_EXTERNAL_DTD, false ); | |
| 98 | dbf.setNamespaceAware( false ); | |
| 99 | final var builder = dbf.newDocumentBuilder(); | |
| 100 | ||
| 101 | final var doc = builder.parse( new ByteArrayInputStream( bytes ) ); | |
| 102 | final var actualSvg = toSvg( doc.getDocumentElement() ); | |
| 103 | ||
| 104 | verifyImage( rasterizeString( actualSvg ) ); | |
| 105 | } | |
| 106 | ||
| 107 | /** | |
| 108 | * Test that an SVG image from a DOM element can be rasterized. | |
| 109 | * | |
| 110 | * @throws IOException Could not write the image. | |
| 111 | */ | |
| 112 | @Test | |
| 113 | public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage() | |
| 114 | throws IOException { | |
| 115 | final var g = new SvgDomGraphics2D(); | |
| 116 | drawGraphics( g ); | |
| 117 | ||
| 118 | final var dom = g.toDom(); | |
| 119 | ||
| 120 | verifyImage( rasterize( dom ) ); | |
| 121 | } | |
| 122 | ||
| 123 | /** | |
| 124 | * Asserts that the given image matches an expected file size. | |
| 125 | * | |
| 126 | * @param image The image to check against the file size. | |
| 127 | * @throws IOException Could not write the image. | |
| 128 | */ | |
| 129 | private void verifyImage( final BufferedImage image ) throws IOException { | |
| 130 | final var file = export( image, "dom.png" ); | |
| 131 | assertEquals( FILESIZE, file.length() ); | |
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * Creates an SVG string for the default equation and font size. | |
| 136 | */ | |
| 137 | private void drawGraphics( final AbstractGraphics2D g ) { | |
| 138 | final var size = 100f; | |
| 139 | final var texFont = new DefaultTeXFont( size ); | |
| 140 | final var env = new TeXEnvironment( texFont ); | |
| 141 | g.scale( size, size ); | |
| 142 | ||
| 143 | final var formula = new TeXFormula( EQUATION ); | |
| 144 | final var box = formula.createBox( env ); | |
| 145 | final var layout = new TeXLayout( box, size ); | |
| 146 | ||
| 147 | g.initialize( layout.getWidth(), layout.getHeight() ); | |
| 148 | box.draw( g, layout.getX(), layout.getY() ); | |
| 149 | } | |
| 150 | ||
| 151 | @SuppressWarnings("SameParameterValue") | |
| 152 | private File export( final BufferedImage image, final String filename ) | |
| 153 | throws IOException { | |
| 154 | final var path = Path.of( DIR_TEMP, filename ); | |
| 155 | final var file = path.toFile(); | |
| 156 | ImageIO.write( image, "png", file ); | |
| 157 | file.deleteOnExit(); | |
| 158 | return file; | |
| 159 | } | |
| 160 | } | |
| 1 | 161 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.tex; | |
| 29 | ||
| 30 | import com.whitemagicsoftware.tex.DefaultTeXFont; | |
| 31 | import com.whitemagicsoftware.tex.TeXEnvironment; | |
| 32 | import com.whitemagicsoftware.tex.TeXFormula; | |
| 33 | import com.whitemagicsoftware.tex.TeXLayout; | |
| 34 | import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D; | |
| 35 | import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D; | |
| 36 | import com.whitemagicsoftware.tex.graphics.SvgGraphics2D; | |
| 37 | import org.junit.jupiter.api.Test; | |
| 38 | import org.xml.sax.SAXException; | |
| 39 | ||
| 40 | import javax.imageio.ImageIO; | |
| 41 | import javax.xml.parsers.DocumentBuilderFactory; | |
| 42 | import javax.xml.parsers.ParserConfigurationException; | |
| 43 | import java.awt.image.BufferedImage; | |
| 44 | import java.io.ByteArrayInputStream; | |
| 45 | import java.io.File; | |
| 46 | import java.io.IOException; | |
| 47 | import java.nio.file.Path; | |
| 48 | ||
| 49 | import static com.scrivenvar.preview.SvgRasterizer.*; | |
| 50 | import static java.lang.System.getProperty; | |
| 51 | import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 52 | ||
| 53 | /** | |
| 54 | * Test that TeX rasterization produces a readable image. | |
| 55 | */ | |
| 56 | public class TeXRasterization { | |
| 57 | private static final String LOAD_EXTERNAL_DTD = | |
| 58 | "http://apache.org/xml/features/nonvalidating/load-external-dtd"; | |
| 59 | ||
| 60 | private static final String EQUATION = | |
| 61 | "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}"; | |
| 62 | ||
| 63 | private static final String DIR_TEMP = getProperty( "java.io.tmpdir" ); | |
| 64 | ||
| 65 | private static final long FILESIZE = 12547; | |
| 66 | ||
| 67 | /** | |
| 68 | * Test that an equation can be converted to a raster image and the | |
| 69 | * final raster image size corresponds to the input equation. This is | |
| 70 | * a simple way to verify that the rasterization process is correct, | |
| 71 | * albeit if any aspect of the SVG algorithm changes (such as padding | |
| 72 | * around the equation), it will cause this test to fail, which is a bit | |
| 73 | * misleading. | |
| 74 | */ | |
| 75 | @Test | |
| 76 | public void test_Rasterize_SimpleFormula_CorrectImageSize() | |
| 77 | throws IOException { | |
| 78 | final var g = new SvgGraphics2D(); | |
| 79 | drawGraphics( g ); | |
| 80 | verifyImage( rasterizeString( g.toString() ) ); | |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Test that an SVG document object model can be parsed and rasterized into | |
| 85 | * an image. | |
| 86 | */ | |
| 87 | @Test | |
| 88 | public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage() | |
| 89 | throws ParserConfigurationException, IOException, SAXException { | |
| 90 | final var g = new SvgGraphics2D(); | |
| 91 | drawGraphics( g ); | |
| 92 | ||
| 93 | final var expectedSvg = g.toString(); | |
| 94 | final var bytes = expectedSvg.getBytes(); | |
| 95 | ||
| 96 | final var dbf = DocumentBuilderFactory.newInstance(); | |
| 97 | dbf.setFeature( LOAD_EXTERNAL_DTD, false ); | |
| 98 | dbf.setNamespaceAware( false ); | |
| 99 | final var builder = dbf.newDocumentBuilder(); | |
| 100 | ||
| 101 | final var doc = builder.parse( new ByteArrayInputStream( bytes ) ); | |
| 102 | final var actualSvg = toSvg( doc.getDocumentElement() ); | |
| 103 | ||
| 104 | verifyImage( rasterizeString( actualSvg ) ); | |
| 105 | } | |
| 106 | ||
| 107 | /** | |
| 108 | * Test that an SVG image from a DOM element can be rasterized. | |
| 109 | * | |
| 110 | * @throws IOException Could not write the image. | |
| 111 | */ | |
| 112 | @Test | |
| 113 | public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage() | |
| 114 | throws IOException { | |
| 115 | final var g = new SvgDomGraphics2D(); | |
| 116 | drawGraphics( g ); | |
| 117 | ||
| 118 | final var dom = g.toDom(); | |
| 119 | ||
| 120 | verifyImage( rasterize( dom ) ); | |
| 121 | } | |
| 122 | ||
| 123 | /** | |
| 124 | * Asserts that the given image matches an expected file size. | |
| 125 | * | |
| 126 | * @param image The image to check against the file size. | |
| 127 | * @throws IOException Could not write the image. | |
| 128 | */ | |
| 129 | private void verifyImage( final BufferedImage image ) throws IOException { | |
| 130 | final var file = export( image, "dom.png" ); | |
| 131 | assertEquals( FILESIZE, file.length() ); | |
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * Creates an SVG string for the default equation and font size. | |
| 136 | */ | |
| 137 | private void drawGraphics( final AbstractGraphics2D g ) { | |
| 138 | final var size = 100f; | |
| 139 | final var texFont = new DefaultTeXFont( size ); | |
| 140 | final var env = new TeXEnvironment( texFont ); | |
| 141 | g.scale( size, size ); | |
| 142 | ||
| 143 | final var formula = new TeXFormula( EQUATION ); | |
| 144 | final var box = formula.createBox( env ); | |
| 145 | final var layout = new TeXLayout( box, size ); | |
| 146 | ||
| 147 | g.initialize( layout.getWidth(), layout.getHeight() ); | |
| 148 | box.draw( g, layout.getX(), layout.getY() ); | |
| 149 | } | |
| 150 | ||
| 151 | @SuppressWarnings("SameParameterValue") | |
| 152 | private File export( final BufferedImage image, final String filename ) | |
| 153 | throws IOException { | |
| 154 | final var path = Path.of( DIR_TEMP, filename ); | |
| 155 | final var file = path.toFile(); | |
| 156 | ImageIO.write( image, "png", file ); | |
| 157 | file.deleteOnExit(); | |
| 158 | return file; | |
| 159 | } | |
| 160 | } | |
| 161 | 1 |