| 9 | 9 | .classpath |
| 10 | 10 | .idea |
| 11 | count | |
| 11 | 12 | themes |
| 12 | 13 | quotes |
| 12 | 12 | * `-i` -- sets the input file name, must be a full path. |
| 13 | 13 | * `-o` -- sets the output file name, can be a relative path. |
| 14 | * `-s` -- sets a variable name and value at build time (dynamic data). | |
| 14 | 15 | |
| 15 | 16 | ## Example usage |
| 12 | 12 | |
| 13 | 13 | import static com.keenwrite.events.StatusEvent.clue; |
| 14 | import static com.keenwrite.io.SysFile.toFile; | |
| 14 | 15 | |
| 15 | 16 | /** |
| ... | ||
| 83 | 84 | |
| 84 | 85 | USER_DATA_DIR = UserDataDir.getAppPath( APP_TITLE_LOWERCASE ); |
| 85 | USER_CACHE_DIR = USER_DATA_DIR.resolve( "cache" ).toFile(); | |
| 86 | USER_CACHE_DIR = toFile( USER_DATA_DIR.resolve( "cache" ) ); | |
| 86 | 87 | |
| 87 | 88 | if( !USER_CACHE_DIR.exists() && !USER_CACHE_DIR.mkdirs() ) { |
| 8 | 8 | import java.nio.file.Path; |
| 9 | 9 | |
| 10 | import static com.keenwrite.io.SysFile.toFile; | |
| 10 | 11 | import static java.lang.String.format; |
| 11 | 12 | import static org.apache.commons.io.FilenameUtils.removeExtension; |
| ... | ||
| 125 | 126 | */ |
| 126 | 127 | public File toExportFilename( final Path path ) { |
| 127 | return toExportFilename( path.toFile() ); | |
| 128 | return toExportFilename( toFile( path ) ); | |
| 128 | 129 | } |
| 129 | 130 | } |
| 74 | 74 | import static com.keenwrite.io.MediaType.*; |
| 75 | 75 | import static com.keenwrite.io.MediaType.TypeName.TEXT; |
| 76 | import static com.keenwrite.io.SysFile.toFile; | |
| 76 | 77 | import static com.keenwrite.preferences.AppKeys.*; |
| 77 | 78 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; |
| ... | ||
| 307 | 308 | else { |
| 308 | 309 | final var parentPath = parent.getAbsolutePath(); |
| 309 | eventFile = Path.of( parentPath, eventUri.getPath() ).toFile(); | |
| 310 | eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) ); | |
| 310 | 311 | } |
| 311 | 312 | } |
| 191 | 191 | |
| 192 | 192 | @CommandLine.Option( |
| 193 | names = {"-s", "--set"}, | |
| 194 | description = | |
| 195 | "Set (or override) a document variable value", | |
| 196 | paramLabel = "key=value" | |
| 197 | ) | |
| 198 | private Map<String, String> mOverrides; | |
| 199 | ||
| 200 | @CommandLine.Option( | |
| 193 | 201 | names = {"--sigil-opening"}, |
| 194 | 202 | description = |
| ... | ||
| 250 | 258 | .with( Mutator::setDefinitions, () -> definitions ) |
| 251 | 259 | .with( Mutator::setMetadata, () -> mMetadata ) |
| 260 | .with( Mutator::setOverrides, () -> mOverrides ) | |
| 252 | 261 | .with( Mutator::setLocale, () -> locale ) |
| 253 | 262 | .with( Mutator::setConcatenate, () -> mConcatenate ) |
| 40 | 40 | public void handle( final StatusEvent event ) { |
| 41 | 41 | if( !mArgs.quiet() ) { |
| 42 | System.out.println( event ); | |
| 42 | System.out.println( event.toString() ); | |
| 43 | System.out.println( event.getProblem() ); | |
| 43 | 44 | } |
| 44 | 45 | } |
| 45 | 46 | |
| 46 | 47 | /** |
| 47 | 48 | * Entry point for running the application in headless mode. |
| 48 | 49 | * |
| 49 | 50 | * @param args The parsed command-line arguments. |
| 50 | 51 | */ |
| 52 | @SuppressWarnings( "ConfusingMainMethod" ) | |
| 51 | 53 | public static void main( final Arguments args ) { |
| 52 | 54 | new HeadlessApp( args ); |
| 13 | 13 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; |
| 14 | 14 | import static com.keenwrite.Bootstrap.USER_DATA_DIR; |
| 15 | import static com.keenwrite.io.SysFile.toFile; | |
| 15 | 16 | import static com.keenwrite.preferences.LocaleScripts.withScript; |
| 16 | 17 | import static java.io.File.separator; |
| ... | ||
| 308 | 309 | } |
| 309 | 310 | |
| 310 | return (fontBase == null | |
| 311 | final var base = fontBase == null | |
| 311 | 312 | ? USER_DATA_DIR.relativize( fontUser ) |
| 312 | : Path.of( fontBase ).resolve( fontUser )).toFile(); | |
| 313 | : Path.of( fontBase ).resolve( fontUser ); | |
| 314 | ||
| 315 | return toFile( base ); | |
| 313 | 316 | } |
| 314 | 317 | } |
| 1 | 1 | package com.keenwrite.dom; |
| 2 | 2 | |
| 3 | import com.keenwrite.io.SysFile; | |
| 3 | 4 | import org.w3c.dom.*; |
| 4 | 5 | import org.xml.sax.InputSource; |
| ... | ||
| 23 | 24 | |
| 24 | 25 | import static com.keenwrite.events.StatusEvent.clue; |
| 26 | import static com.keenwrite.io.SysFile.toFile; | |
| 25 | 27 | import static java.nio.charset.StandardCharsets.UTF_16; |
| 26 | 28 | import static java.nio.charset.StandardCharsets.UTF_8; |
| ... | ||
| 245 | 247 | |
| 246 | 248 | final var target = new StreamResult( sOutput ); |
| 247 | final var source = sDocumentBuilder.parse( path.toFile() ); | |
| 249 | final var source = sDocumentBuilder.parse( toFile( path ) ); | |
| 248 | 250 | |
| 249 | 251 | transform( source, target ); |
| 13 | 13 | import static com.keenwrite.constants.Constants.DEFAULT_CHARSET; |
| 14 | 14 | import static com.keenwrite.events.StatusEvent.clue; |
| 15 | import static com.keenwrite.io.SysFile.toFile; | |
| 15 | 16 | import static java.nio.charset.Charset.forName; |
| 16 | 17 | import static java.nio.file.Files.readAllBytes; |
| ... | ||
| 114 | 115 | */ |
| 115 | 116 | default Charset open( final Path path ) { |
| 116 | final var file = path.toFile(); | |
| 117 | final var file = toFile( path ); | |
| 117 | 118 | Charset encoding = DEFAULT_CHARSET; |
| 118 | 119 | |
| 13 | 13 | import java.util.concurrent.ConcurrentHashMap; |
| 14 | 14 | |
| 15 | import static com.keenwrite.io.SysFile.toFile; | |
| 15 | 16 | import static java.nio.file.FileSystems.getDefault; |
| 16 | 17 | import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; |
| ... | ||
| 72 | 73 | final var watchable = (Path) watchKey.watchable(); |
| 73 | 74 | final var context = (Path) pollEvent.context(); |
| 74 | final var file = watchable.resolve( context ).toFile(); | |
| 75 | final var file = toFile( watchable.resolve( context ) ); | |
| 75 | 76 | |
| 76 | 77 | if( mWatched.containsKey( file ) ) { |
| 9 | 9 | import static com.keenwrite.io.MediaType.TypeName.*; |
| 10 | 10 | import static com.keenwrite.io.MediaTypeExtension.fromExtension; |
| 11 | import static org.apache.commons.io.FilenameUtils.getExtension; | |
| 12 | ||
| 13 | /** | |
| 14 | * Defines various file formats and format contents. | |
| 15 | * | |
| 16 | * @see | |
| 17 | * <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA | |
| 18 | * Media Types</a> | |
| 19 | */ | |
| 20 | @SuppressWarnings( "SpellCheckingInspection" ) | |
| 21 | public enum MediaType { | |
| 22 | APP_DOCUMENT_OUTLINE( APPLICATION, "x-document-outline" ), | |
| 23 | APP_DOCUMENT_STATISTICS( APPLICATION, "x-document-statistics" ), | |
| 24 | APP_FILE_MANAGER( APPLICATION, "x-file-manager" ), | |
| 25 | ||
| 26 | APP_ACAD( APPLICATION, "acad" ), | |
| 27 | APP_JAVA_OBJECT( APPLICATION, "x-java-serialized-object" ), | |
| 28 | APP_JAVA( APPLICATION, "java" ), | |
| 29 | APP_PS( APPLICATION, "postscript" ), | |
| 30 | APP_EPS( APPLICATION, "eps" ), | |
| 31 | APP_PDF( APPLICATION, "pdf" ), | |
| 32 | APP_ZIP( APPLICATION, "zip" ), | |
| 33 | ||
| 34 | /* | |
| 35 | * Standard font types. | |
| 36 | */ | |
| 37 | FONT_OTF( "otf" ), | |
| 38 | FONT_TTF( "ttf" ), | |
| 39 | ||
| 40 | /* | |
| 41 | * Standard image types. | |
| 42 | */ | |
| 43 | IMAGE_APNG( "apng" ), | |
| 44 | IMAGE_ACES( "aces" ), | |
| 45 | IMAGE_AVCI( "avci" ), | |
| 46 | IMAGE_AVCS( "avcs" ), | |
| 47 | IMAGE_BMP( "bmp" ), | |
| 48 | IMAGE_CGM( "cgm" ), | |
| 49 | IMAGE_DICOM_RLE( "dicom_rle" ), | |
| 50 | IMAGE_EMF( "emf" ), | |
| 51 | IMAGE_EXAMPLE( "example" ), | |
| 52 | IMAGE_FITS( "fits" ), | |
| 53 | IMAGE_G3FAX( "g3fax" ), | |
| 54 | IMAGE_GIF( "gif" ), | |
| 55 | IMAGE_HEIC( "heic" ), | |
| 56 | IMAGE_HEIF( "heif" ), | |
| 57 | IMAGE_HEJ2K( "hej2k" ), | |
| 58 | IMAGE_HSJ2( "hsj2" ), | |
| 59 | IMAGE_X_ICON( "x-icon" ), | |
| 60 | IMAGE_JLS( "jls" ), | |
| 61 | IMAGE_JP2( "jp2" ), | |
| 62 | IMAGE_JPEG( "jpeg" ), | |
| 63 | IMAGE_JPH( "jph" ), | |
| 64 | IMAGE_JPHC( "jphc" ), | |
| 65 | IMAGE_JPM( "jpm" ), | |
| 66 | IMAGE_JPX( "jpx" ), | |
| 67 | IMAGE_JXR( "jxr" ), | |
| 68 | IMAGE_JXRA( "jxrA" ), | |
| 69 | IMAGE_JXRS( "jxrS" ), | |
| 70 | IMAGE_JXS( "jxs" ), | |
| 71 | IMAGE_JXSC( "jxsc" ), | |
| 72 | IMAGE_JXSI( "jxsi" ), | |
| 73 | IMAGE_JXSS( "jxss" ), | |
| 74 | IMAGE_KTX( "ktx" ), | |
| 75 | IMAGE_KTX2( "ktx2" ), | |
| 76 | IMAGE_NAPLPS( "naplps" ), | |
| 77 | IMAGE_PNG( "png" ), | |
| 78 | IMAGE_PHOTOSHOP( "photoshop" ), | |
| 79 | IMAGE_SVG_XML( "svg+xml" ), | |
| 80 | IMAGE_T38( "t38" ), | |
| 81 | IMAGE_TIFF( "tiff" ), | |
| 82 | IMAGE_WEBP( "webp" ), | |
| 83 | IMAGE_WMF( "wmf" ), | |
| 84 | IMAGE_X_BITMAP( "x-xbitmap" ), | |
| 85 | IMAGE_X_PIXMAP( "x-xpixmap" ), | |
| 86 | ||
| 87 | /* | |
| 88 | * Standard audio types. | |
| 89 | */ | |
| 90 | AUDIO_SIMPLE( AUDIO, "basic" ), | |
| 91 | AUDIO_MP3( AUDIO, "mp3" ), | |
| 92 | AUDIO_WAV( AUDIO, "x-wav" ), | |
| 93 | ||
| 94 | /* | |
| 95 | * Standard video types. | |
| 96 | */ | |
| 97 | VIDEO_MNG( VIDEO, "x-mng" ), | |
| 98 | ||
| 99 | /* | |
| 100 | * Document types for editing or displaying documents, mix of standard and | |
| 101 | * application-specific. The order that these are declared reflect in the | |
| 102 | * ordinal value used during comparisons. | |
| 103 | */ | |
| 104 | TEXT_YAML( TEXT, "yaml" ), | |
| 105 | TEXT_PLAIN( TEXT, "plain" ), | |
| 106 | TEXT_MARKDOWN( TEXT, "markdown" ), | |
| 107 | TEXT_R_MARKDOWN( TEXT, "R+markdown" ), | |
| 108 | TEXT_PROPERTIES( TEXT, "x-java-properties" ), | |
| 109 | TEXT_HTML( TEXT, "html" ), | |
| 110 | TEXT_XHTML( TEXT, "xhtml+xml" ), | |
| 111 | TEXT_XML( TEXT, "xml" ), | |
| 112 | ||
| 113 | /* | |
| 114 | * When all other lights go out. | |
| 115 | */ | |
| 116 | UNDEFINED( TypeName.UNDEFINED, "undefined" ); | |
| 117 | ||
| 118 | /** | |
| 119 | * The IANA-defined types. | |
| 120 | */ | |
| 121 | public enum TypeName { | |
| 122 | APPLICATION, | |
| 123 | AUDIO, | |
| 124 | IMAGE, | |
| 125 | TEXT, | |
| 126 | UNDEFINED, | |
| 127 | VIDEO | |
| 128 | } | |
| 129 | ||
| 130 | /** | |
| 131 | * The fully qualified IANA-defined media type. | |
| 132 | */ | |
| 133 | private final String mMediaType; | |
| 134 | ||
| 135 | /** | |
| 136 | * The IANA-defined type name. | |
| 137 | */ | |
| 138 | private final TypeName mTypeName; | |
| 139 | ||
| 140 | /** | |
| 141 | * The IANA-defined subtype name. | |
| 142 | */ | |
| 143 | private final String mSubtype; | |
| 144 | ||
| 145 | /** | |
| 146 | * Constructs an instance using the default type name of "image". | |
| 147 | * | |
| 148 | * @param subtype The image subtype name. | |
| 149 | */ | |
| 150 | MediaType( final String subtype ) { | |
| 151 | this( IMAGE, subtype ); | |
| 152 | } | |
| 153 | ||
| 154 | /** | |
| 155 | * Constructs an instance using an IANA-defined type and subtype pair. | |
| 156 | * | |
| 157 | * @param typeName The media type's type name. | |
| 158 | * @param subtype The media type's subtype name. | |
| 159 | */ | |
| 160 | MediaType( final TypeName typeName, final String subtype ) { | |
| 161 | mTypeName = typeName; | |
| 162 | mSubtype = subtype; | |
| 163 | mMediaType = typeName.toString().toLowerCase() + '/' + subtype; | |
| 164 | } | |
| 165 | ||
| 166 | /** | |
| 167 | * Returns the {@link MediaType} associated with the given file. | |
| 168 | * | |
| 169 | * @param file Has a file name that may contain an extension associated with | |
| 170 | * a known {@link MediaType}. | |
| 171 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 172 | * assigned, otherwise the {@link MediaType} associated with this | |
| 173 | * {@link File}'s file name extension. | |
| 174 | */ | |
| 175 | public static MediaType fromFilename( final File file ) { | |
| 176 | assert file != null; | |
| 177 | return fromFilename( file.getName() ); | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Returns the {@link MediaType} associated with the given file name. | |
| 182 | * | |
| 183 | * @param filename The file name that may contain an extension associated | |
| 184 | * with a known {@link MediaType}. | |
| 185 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 186 | * assigned, otherwise the {@link MediaType} associated with this | |
| 187 | * {@link URL}'s file name extension. | |
| 188 | */ | |
| 189 | public static MediaType fromFilename( final String filename ) { | |
| 190 | assert filename != null; | |
| 191 | return fromExtension( getExtension( filename ) ); | |
| 192 | } | |
| 193 | ||
| 194 | /** | |
| 195 | * Returns the {@link MediaType} associated with the path to a file. | |
| 196 | * | |
| 197 | * @param path Has a file name that may contain an extension associated with | |
| 198 | * a known {@link MediaType}. | |
| 199 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 200 | * assigned, otherwise the {@link MediaType} associated with this | |
| 201 | * {@link File}'s file name extension. | |
| 202 | */ | |
| 203 | public static MediaType fromFilename( final Path path ) { | |
| 204 | assert path != null; | |
| 205 | return fromFilename( path.toFile() ); | |
| 206 | } | |
| 207 | ||
| 208 | /** | |
| 209 | * Determines the media type an IANA-defined, semi-colon-separated string. | |
| 210 | * This is often used after making an HTTP request to extract the type | |
| 211 | * and subtype from the content-type. | |
| 212 | * | |
| 213 | * @param header The content-type header value, may be {@code null}. | |
| 214 | * @return The data type for the resource or {@link MediaType#UNDEFINED} if | |
| 215 | * unmapped. | |
| 216 | */ | |
| 217 | public static MediaType valueFrom( String header ) { | |
| 218 | if( header == null || header.isBlank() ) { | |
| 219 | return UNDEFINED; | |
| 220 | } | |
| 221 | ||
| 222 | // Trim off the character encoding. | |
| 223 | var i = header.indexOf( ';' ); | |
| 224 | header = header.substring( 0, i == -1 ? header.length() : i ); | |
| 225 | ||
| 226 | // Split the type and subtype. | |
| 227 | i = header.indexOf( '/' ); | |
| 228 | i = i == -1 ? header.length() : i; | |
| 229 | final var type = header.substring( 0, i ); | |
| 230 | final var subtype = header.substring( i + 1 ); | |
| 231 | ||
| 232 | return valueFrom( type, subtype ); | |
| 233 | } | |
| 234 | ||
| 235 | /** | |
| 236 | * Returns the {@link MediaType} for the given type and subtype names. | |
| 237 | * | |
| 238 | * @param type The IANA-defined type name. | |
| 239 | * @param subtype The IANA-defined subtype name. | |
| 240 | * @return {@link MediaType#UNDEFINED} if there is no {@link MediaType} that | |
| 241 | * matches the given type and subtype names. | |
| 242 | */ | |
| 243 | public static MediaType valueFrom( | |
| 244 | final String type, final String subtype ) { | |
| 245 | assert type != null; | |
| 246 | assert subtype != null; | |
| 247 | ||
| 248 | for( final var mediaType : values() ) { | |
| 249 | if( mediaType.equals( type, subtype ) ) { | |
| 250 | return mediaType; | |
| 251 | } | |
| 252 | } | |
| 253 | ||
| 254 | return UNDEFINED; | |
| 255 | } | |
| 256 | ||
| 257 | /** | |
| 258 | * Answers whether the given type and subtype names equal this enumerated | |
| 259 | * value. This performs a case-insensitive comparison. | |
| 260 | * | |
| 261 | * @param type The type name to compare against this {@link MediaType}. | |
| 262 | * @param subtype The subtype name to compare against this {@link MediaType}. | |
| 263 | * @return {@code true} when the type and subtype name match. | |
| 264 | */ | |
| 265 | public boolean equals( final String type, final String subtype ) { | |
| 266 | assert type != null; | |
| 267 | assert subtype != null; | |
| 268 | ||
| 269 | return mTypeName.name().equalsIgnoreCase( type ) && | |
| 270 | mSubtype.equalsIgnoreCase( subtype ); | |
| 271 | } | |
| 272 | ||
| 273 | /** | |
| 274 | * Answers whether the given {@link TypeName} matches this type name. | |
| 275 | * | |
| 276 | * @param typeName The {@link TypeName} to compare against the internal value. | |
| 277 | * @return {@code true} if the given value is the same IANA-defined type name. | |
| 278 | */ | |
| 279 | @SuppressWarnings( "unused" ) | |
| 280 | public boolean isType( final TypeName typeName ) { | |
| 281 | return mTypeName == typeName; | |
| 282 | } | |
| 283 | ||
| 284 | /** | |
| 285 | * Answers whether this instance is a scalable vector graphic. | |
| 286 | * | |
| 287 | * @return {@code true} if this instance represents an SVG object. | |
| 288 | */ | |
| 289 | public boolean isSvg() { | |
| 290 | return equals( IMAGE_SVG_XML ); | |
| 291 | } | |
| 292 | ||
| 293 | /** | |
| 294 | * Answers whether this instance is an image, vector or raster. | |
| 295 | * | |
| 296 | * @return {@code true} if this instance represents any type of image. | |
| 297 | */ | |
| 298 | public boolean isImage() { | |
| 299 | return isType( IMAGE ); | |
| 300 | } | |
| 301 | ||
| 302 | public boolean isUndefined() { | |
| 303 | return equals( UNDEFINED ); | |
| 304 | } | |
| 305 | ||
| 306 | /** | |
| 307 | * Returns the IANA-defined subtype classification. Primarily used by | |
| 308 | * {@link MediaTypeExtension} to initialize associations where the subtype | |
| 309 | * name and the file name extension have a 1:1 mapping. | |
| 310 | * | |
| 311 | * @return The IANA subtype value. | |
| 312 | */ | |
| 313 | public String getSubtype() { | |
| 314 | return mSubtype; | |
| 315 | } | |
| 316 | ||
| 317 | /** | |
| 318 | * Creates a temporary {@link File} that starts with the given prefix. | |
| 319 | * | |
| 320 | * @param prefix The file name begins with this string (empty is allowed). | |
| 321 | * @param directory The directory wherein the file is created. | |
| 322 | * @return The fully qualified path to the temporary file. | |
| 323 | * @throws IOException Could not create the temporary file. | |
| 324 | */ | |
| 325 | public Path createTempFile( | |
| 326 | final String prefix, | |
| 327 | final Path directory ) throws IOException { | |
| 328 | return createTempFile( prefix, directory, false ); | |
| 329 | } | |
| 330 | ||
| 331 | /** | |
| 332 | * Creates a temporary {@link File} that starts with the given prefix. | |
| 333 | * | |
| 334 | * @param prefix The file name begins with this string (empty is allowed). | |
| 335 | * @param directory The directory wherein the file is created. | |
| 336 | * @param purge Set to {@code true} to delete the file on exit. | |
| 337 | * @return The fully qualified path to the temporary file. | |
| 338 | * @throws IOException Could not create the temporary file. | |
| 339 | */ | |
| 340 | public Path createTempFile( | |
| 341 | final String prefix, | |
| 342 | final Path directory, | |
| 343 | final boolean purge ) | |
| 344 | throws IOException { | |
| 345 | assert prefix != null; | |
| 346 | ||
| 347 | final var suffix = '.' + MediaTypeExtension | |
| 348 | .valueFrom( this ) | |
| 349 | .getExtension(); | |
| 350 | ||
| 351 | final var file = File.createTempFile( prefix, suffix, directory.toFile() ); | |
| 11 | import static com.keenwrite.io.SysFile.toFile; | |
| 12 | import static org.apache.commons.io.FilenameUtils.getExtension; | |
| 13 | ||
| 14 | /** | |
| 15 | * Defines various file formats and format contents. | |
| 16 | * | |
| 17 | * @see | |
| 18 | * <a href="https://www.iana.org/assignments/media-types/media-types.xhtml">IANA | |
| 19 | * Media Types</a> | |
| 20 | */ | |
| 21 | @SuppressWarnings( "SpellCheckingInspection" ) | |
| 22 | public enum MediaType { | |
| 23 | APP_DOCUMENT_OUTLINE( APPLICATION, "x-document-outline" ), | |
| 24 | APP_DOCUMENT_STATISTICS( APPLICATION, "x-document-statistics" ), | |
| 25 | APP_FILE_MANAGER( APPLICATION, "x-file-manager" ), | |
| 26 | ||
| 27 | APP_ACAD( APPLICATION, "acad" ), | |
| 28 | APP_JAVA_OBJECT( APPLICATION, "x-java-serialized-object" ), | |
| 29 | APP_JAVA( APPLICATION, "java" ), | |
| 30 | APP_PS( APPLICATION, "postscript" ), | |
| 31 | APP_EPS( APPLICATION, "eps" ), | |
| 32 | APP_PDF( APPLICATION, "pdf" ), | |
| 33 | APP_ZIP( APPLICATION, "zip" ), | |
| 34 | ||
| 35 | /* | |
| 36 | * Standard font types. | |
| 37 | */ | |
| 38 | FONT_OTF( "otf" ), | |
| 39 | FONT_TTF( "ttf" ), | |
| 40 | ||
| 41 | /* | |
| 42 | * Standard image types. | |
| 43 | */ | |
| 44 | IMAGE_APNG( "apng" ), | |
| 45 | IMAGE_ACES( "aces" ), | |
| 46 | IMAGE_AVCI( "avci" ), | |
| 47 | IMAGE_AVCS( "avcs" ), | |
| 48 | IMAGE_BMP( "bmp" ), | |
| 49 | IMAGE_CGM( "cgm" ), | |
| 50 | IMAGE_DICOM_RLE( "dicom_rle" ), | |
| 51 | IMAGE_EMF( "emf" ), | |
| 52 | IMAGE_EXAMPLE( "example" ), | |
| 53 | IMAGE_FITS( "fits" ), | |
| 54 | IMAGE_G3FAX( "g3fax" ), | |
| 55 | IMAGE_GIF( "gif" ), | |
| 56 | IMAGE_HEIC( "heic" ), | |
| 57 | IMAGE_HEIF( "heif" ), | |
| 58 | IMAGE_HEJ2K( "hej2k" ), | |
| 59 | IMAGE_HSJ2( "hsj2" ), | |
| 60 | IMAGE_X_ICON( "x-icon" ), | |
| 61 | IMAGE_JLS( "jls" ), | |
| 62 | IMAGE_JP2( "jp2" ), | |
| 63 | IMAGE_JPEG( "jpeg" ), | |
| 64 | IMAGE_JPH( "jph" ), | |
| 65 | IMAGE_JPHC( "jphc" ), | |
| 66 | IMAGE_JPM( "jpm" ), | |
| 67 | IMAGE_JPX( "jpx" ), | |
| 68 | IMAGE_JXR( "jxr" ), | |
| 69 | IMAGE_JXRA( "jxrA" ), | |
| 70 | IMAGE_JXRS( "jxrS" ), | |
| 71 | IMAGE_JXS( "jxs" ), | |
| 72 | IMAGE_JXSC( "jxsc" ), | |
| 73 | IMAGE_JXSI( "jxsi" ), | |
| 74 | IMAGE_JXSS( "jxss" ), | |
| 75 | IMAGE_KTX( "ktx" ), | |
| 76 | IMAGE_KTX2( "ktx2" ), | |
| 77 | IMAGE_NAPLPS( "naplps" ), | |
| 78 | IMAGE_PNG( "png" ), | |
| 79 | IMAGE_PHOTOSHOP( "photoshop" ), | |
| 80 | IMAGE_SVG_XML( "svg+xml" ), | |
| 81 | IMAGE_T38( "t38" ), | |
| 82 | IMAGE_TIFF( "tiff" ), | |
| 83 | IMAGE_WEBP( "webp" ), | |
| 84 | IMAGE_WMF( "wmf" ), | |
| 85 | IMAGE_X_BITMAP( "x-xbitmap" ), | |
| 86 | IMAGE_X_PIXMAP( "x-xpixmap" ), | |
| 87 | ||
| 88 | /* | |
| 89 | * Standard audio types. | |
| 90 | */ | |
| 91 | AUDIO_SIMPLE( AUDIO, "basic" ), | |
| 92 | AUDIO_MP3( AUDIO, "mp3" ), | |
| 93 | AUDIO_WAV( AUDIO, "x-wav" ), | |
| 94 | ||
| 95 | /* | |
| 96 | * Standard video types. | |
| 97 | */ | |
| 98 | VIDEO_MNG( VIDEO, "x-mng" ), | |
| 99 | ||
| 100 | /* | |
| 101 | * Document types for editing or displaying documents, mix of standard and | |
| 102 | * application-specific. The order that these are declared reflect in the | |
| 103 | * ordinal value used during comparisons. | |
| 104 | */ | |
| 105 | TEXT_YAML( TEXT, "yaml" ), | |
| 106 | TEXT_PLAIN( TEXT, "plain" ), | |
| 107 | TEXT_MARKDOWN( TEXT, "markdown" ), | |
| 108 | TEXT_R_MARKDOWN( TEXT, "R+markdown" ), | |
| 109 | TEXT_PROPERTIES( TEXT, "x-java-properties" ), | |
| 110 | TEXT_HTML( TEXT, "html" ), | |
| 111 | TEXT_XHTML( TEXT, "xhtml+xml" ), | |
| 112 | TEXT_XML( TEXT, "xml" ), | |
| 113 | ||
| 114 | /* | |
| 115 | * When all other lights go out. | |
| 116 | */ | |
| 117 | UNDEFINED( TypeName.UNDEFINED, "undefined" ); | |
| 118 | ||
| 119 | /** | |
| 120 | * The IANA-defined types. | |
| 121 | */ | |
| 122 | public enum TypeName { | |
| 123 | APPLICATION, | |
| 124 | AUDIO, | |
| 125 | IMAGE, | |
| 126 | TEXT, | |
| 127 | UNDEFINED, | |
| 128 | VIDEO | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * The fully qualified IANA-defined media type. | |
| 133 | */ | |
| 134 | private final String mMediaType; | |
| 135 | ||
| 136 | /** | |
| 137 | * The IANA-defined type name. | |
| 138 | */ | |
| 139 | private final TypeName mTypeName; | |
| 140 | ||
| 141 | /** | |
| 142 | * The IANA-defined subtype name. | |
| 143 | */ | |
| 144 | private final String mSubtype; | |
| 145 | ||
| 146 | /** | |
| 147 | * Constructs an instance using the default type name of "image". | |
| 148 | * | |
| 149 | * @param subtype The image subtype name. | |
| 150 | */ | |
| 151 | MediaType( final String subtype ) { | |
| 152 | this( IMAGE, subtype ); | |
| 153 | } | |
| 154 | ||
| 155 | /** | |
| 156 | * Constructs an instance using an IANA-defined type and subtype pair. | |
| 157 | * | |
| 158 | * @param typeName The media type's type name. | |
| 159 | * @param subtype The media type's subtype name. | |
| 160 | */ | |
| 161 | MediaType( final TypeName typeName, final String subtype ) { | |
| 162 | mTypeName = typeName; | |
| 163 | mSubtype = subtype; | |
| 164 | mMediaType = typeName.toString().toLowerCase() + '/' + subtype; | |
| 165 | } | |
| 166 | ||
| 167 | /** | |
| 168 | * Returns the {@link MediaType} associated with the given file. | |
| 169 | * | |
| 170 | * @param file Has a file name that may contain an extension associated with | |
| 171 | * a known {@link MediaType}. | |
| 172 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 173 | * assigned, otherwise the {@link MediaType} associated with this | |
| 174 | * {@link File}'s file name extension. | |
| 175 | */ | |
| 176 | public static MediaType fromFilename( final File file ) { | |
| 177 | assert file != null; | |
| 178 | return fromFilename( file.getName() ); | |
| 179 | } | |
| 180 | ||
| 181 | /** | |
| 182 | * Returns the {@link MediaType} associated with the given file name. | |
| 183 | * | |
| 184 | * @param filename The file name that may contain an extension associated | |
| 185 | * with a known {@link MediaType}. | |
| 186 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 187 | * assigned, otherwise the {@link MediaType} associated with this | |
| 188 | * {@link URL}'s file name extension. | |
| 189 | */ | |
| 190 | public static MediaType fromFilename( final String filename ) { | |
| 191 | assert filename != null; | |
| 192 | return fromExtension( getExtension( filename ) ); | |
| 193 | } | |
| 194 | ||
| 195 | /** | |
| 196 | * Returns the {@link MediaType} associated with the path to a file. | |
| 197 | * | |
| 198 | * @param path Has a file name that may contain an extension associated with | |
| 199 | * a known {@link MediaType}. | |
| 200 | * @return {@link MediaType#UNDEFINED} if the extension has not been | |
| 201 | * assigned, otherwise the {@link MediaType} associated with this | |
| 202 | * {@link File}'s file name extension. | |
| 203 | */ | |
| 204 | public static MediaType fromFilename( final Path path ) { | |
| 205 | assert path != null; | |
| 206 | return fromFilename( path.toFile() ); | |
| 207 | } | |
| 208 | ||
| 209 | /** | |
| 210 | * Determines the media type an IANA-defined, semi-colon-separated string. | |
| 211 | * This is often used after making an HTTP request to extract the type | |
| 212 | * and subtype from the content-type. | |
| 213 | * | |
| 214 | * @param header The content-type header value, may be {@code null}. | |
| 215 | * @return The data type for the resource or {@link MediaType#UNDEFINED} if | |
| 216 | * unmapped. | |
| 217 | */ | |
| 218 | public static MediaType valueFrom( String header ) { | |
| 219 | if( header == null || header.isBlank() ) { | |
| 220 | return UNDEFINED; | |
| 221 | } | |
| 222 | ||
| 223 | // Trim off the character encoding. | |
| 224 | var i = header.indexOf( ';' ); | |
| 225 | header = header.substring( 0, i == -1 ? header.length() : i ); | |
| 226 | ||
| 227 | // Split the type and subtype. | |
| 228 | i = header.indexOf( '/' ); | |
| 229 | i = i == -1 ? header.length() : i; | |
| 230 | final var type = header.substring( 0, i ); | |
| 231 | final var subtype = header.substring( i + 1 ); | |
| 232 | ||
| 233 | return valueFrom( type, subtype ); | |
| 234 | } | |
| 235 | ||
| 236 | /** | |
| 237 | * Returns the {@link MediaType} for the given type and subtype names. | |
| 238 | * | |
| 239 | * @param type The IANA-defined type name. | |
| 240 | * @param subtype The IANA-defined subtype name. | |
| 241 | * @return {@link MediaType#UNDEFINED} if there is no {@link MediaType} that | |
| 242 | * matches the given type and subtype names. | |
| 243 | */ | |
| 244 | public static MediaType valueFrom( | |
| 245 | final String type, final String subtype ) { | |
| 246 | assert type != null; | |
| 247 | assert subtype != null; | |
| 248 | ||
| 249 | for( final var mediaType : values() ) { | |
| 250 | if( mediaType.equals( type, subtype ) ) { | |
| 251 | return mediaType; | |
| 252 | } | |
| 253 | } | |
| 254 | ||
| 255 | return UNDEFINED; | |
| 256 | } | |
| 257 | ||
| 258 | /** | |
| 259 | * Answers whether the given type and subtype names equal this enumerated | |
| 260 | * value. This performs a case-insensitive comparison. | |
| 261 | * | |
| 262 | * @param type The type name to compare against this {@link MediaType}. | |
| 263 | * @param subtype The subtype name to compare against this {@link MediaType}. | |
| 264 | * @return {@code true} when the type and subtype name match. | |
| 265 | */ | |
| 266 | public boolean equals( final String type, final String subtype ) { | |
| 267 | assert type != null; | |
| 268 | assert subtype != null; | |
| 269 | ||
| 270 | return mTypeName.name().equalsIgnoreCase( type ) && | |
| 271 | mSubtype.equalsIgnoreCase( subtype ); | |
| 272 | } | |
| 273 | ||
| 274 | /** | |
| 275 | * Answers whether the given {@link TypeName} matches this type name. | |
| 276 | * | |
| 277 | * @param typeName The {@link TypeName} to compare against the internal value. | |
| 278 | * @return {@code true} if the given value is the same IANA-defined type name. | |
| 279 | */ | |
| 280 | @SuppressWarnings( "unused" ) | |
| 281 | public boolean isType( final TypeName typeName ) { | |
| 282 | return mTypeName == typeName; | |
| 283 | } | |
| 284 | ||
| 285 | /** | |
| 286 | * Answers whether this instance is a scalable vector graphic. | |
| 287 | * | |
| 288 | * @return {@code true} if this instance represents an SVG object. | |
| 289 | */ | |
| 290 | public boolean isSvg() { | |
| 291 | return equals( IMAGE_SVG_XML ); | |
| 292 | } | |
| 293 | ||
| 294 | /** | |
| 295 | * Answers whether this instance is an image, vector or raster. | |
| 296 | * | |
| 297 | * @return {@code true} if this instance represents any type of image. | |
| 298 | */ | |
| 299 | public boolean isImage() { | |
| 300 | return isType( IMAGE ); | |
| 301 | } | |
| 302 | ||
| 303 | public boolean isUndefined() { | |
| 304 | return equals( UNDEFINED ); | |
| 305 | } | |
| 306 | ||
| 307 | /** | |
| 308 | * Returns the IANA-defined subtype classification. Primarily used by | |
| 309 | * {@link MediaTypeExtension} to initialize associations where the subtype | |
| 310 | * name and the file name extension have a 1:1 mapping. | |
| 311 | * | |
| 312 | * @return The IANA subtype value. | |
| 313 | */ | |
| 314 | public String getSubtype() { | |
| 315 | return mSubtype; | |
| 316 | } | |
| 317 | ||
| 318 | /** | |
| 319 | * Creates a temporary {@link File} that starts with the given prefix. | |
| 320 | * | |
| 321 | * @param prefix The file name begins with this string (empty is allowed). | |
| 322 | * @param directory The directory wherein the file is created. | |
| 323 | * @return The fully qualified path to the temporary file. | |
| 324 | * @throws IOException Could not create the temporary file. | |
| 325 | */ | |
| 326 | public Path createTempFile( | |
| 327 | final String prefix, | |
| 328 | final Path directory ) throws IOException { | |
| 329 | return createTempFile( prefix, directory, false ); | |
| 330 | } | |
| 331 | ||
| 332 | /** | |
| 333 | * Creates a temporary {@link File} that starts with the given prefix. | |
| 334 | * | |
| 335 | * @param prefix The file name begins with this string (empty is allowed). | |
| 336 | * @param directory The directory wherein the file is created. | |
| 337 | * @param purge Set to {@code true} to delete the file on exit. | |
| 338 | * @return The fully qualified path to the temporary file. | |
| 339 | * @throws IOException Could not create the temporary file. | |
| 340 | */ | |
| 341 | public Path createTempFile( | |
| 342 | final String prefix, | |
| 343 | final Path directory, | |
| 344 | final boolean purge ) | |
| 345 | throws IOException { | |
| 346 | assert prefix != null; | |
| 347 | ||
| 348 | final var suffix = '.' + MediaTypeExtension | |
| 349 | .valueFrom( this ) | |
| 350 | .getExtension(); | |
| 351 | ||
| 352 | final var file = File.createTempFile( prefix, suffix, toFile( directory ) ); | |
| 352 | 353 | |
| 353 | 354 | if( purge ) { |
| 9 | 9 | |
| 10 | 10 | import static com.keenwrite.io.MediaType.*; |
| 11 | import static com.keenwrite.io.SysFile.toFile; | |
| 11 | 12 | import static java.util.List.of; |
| 12 | 13 | |
| ... | ||
| 103 | 104 | |
| 104 | 105 | public static MediaType fromPath( final Path path ) { |
| 105 | return fromFile( path.toFile() ); | |
| 106 | return fromFile( toFile( path ) ); | |
| 106 | 107 | } |
| 107 | 108 | |
| 267 | 267 | * deleted or moved. |
| 268 | 268 | * |
| 269 | * @param path The path to verify existence. | |
| 269 | * @param path The path to verify existence, may be null. | |
| 270 | 270 | * @return The given path, if it exists, otherwise the user's home directory. |
| 271 | 271 | */ |
| 272 | 272 | public static Path normalize( final Path path ) { |
| 273 | assert path != null; | |
| 273 | return path == null | |
| 274 | ? USER_DIRECTORY.toPath() | |
| 275 | : path.toFile().exists() | |
| 276 | ? path | |
| 277 | : USER_DIRECTORY.toPath(); | |
| 278 | } | |
| 274 | 279 | |
| 275 | return path.toFile().exists() ? path : USER_DIRECTORY.toPath(); | |
| 280 | public static File toFile( final Path path ) { | |
| 281 | return path == null | |
| 282 | ? USER_DIRECTORY | |
| 283 | : path.toFile(); | |
| 276 | 284 | } |
| 277 | 285 |
| 4 | 4 | import java.nio.file.Path; |
| 5 | 5 | |
| 6 | import static com.keenwrite.io.SysFile.toFile; | |
| 6 | 7 | import static java.lang.System.getProperty; |
| 7 | 8 | import static java.lang.System.getenv; |
| ... | ||
| 131 | 132 | */ |
| 132 | 133 | private static boolean ensureExists( final Path path ) { |
| 133 | final var file = path.toFile(); | |
| 134 | final var file = toFile( path ); | |
| 134 | 135 | return file.exists() || file.mkdirs(); |
| 135 | 136 | } |
| 12 | 12 | import java.util.zip.ZipFile; |
| 13 | 13 | |
| 14 | import static com.keenwrite.io.SysFile.toFile; | |
| 14 | 15 | import static java.nio.file.Files.createDirectories; |
| 15 | 16 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; |
| ... | ||
| 105 | 106 | final BiConsumer<ZipFile, ZipEntry> consumer ) |
| 106 | 107 | throws IOException { |
| 107 | assert zipPath.toFile().isFile(); | |
| 108 | assert toFile( zipPath ).isFile(); | |
| 108 | 109 | |
| 109 | try( final var zipFile = new ZipFile( zipPath.toFile() ) ) { | |
| 110 | try( final var zipFile = new ZipFile( toFile( zipPath ) ) ) { | |
| 110 | 111 | final var entries = zipFile.entries(); |
| 111 | 112 | |
| 562 | 562 | public <K, V> Map<K, V> getMetadata() { |
| 563 | 563 | final var metadata = listsProperty( KEY_DOC_META ); |
| 564 | final var map = new HashMap<K, V>( metadata.size() ); | |
| 564 | final HashMap<K, V> map; | |
| 565 | 565 | |
| 566 | metadata.forEach( | |
| 567 | entry -> map.put( (K) entry.getKey(), (V) entry.getValue() ) | |
| 568 | ); | |
| 566 | if( metadata != null ) { | |
| 567 | map = new HashMap<>( metadata.size() ); | |
| 568 | ||
| 569 | metadata.forEach( | |
| 570 | entry -> map.put( (K) entry.getKey(), (V) entry.getValue() ) | |
| 571 | ); | |
| 572 | } | |
| 573 | else { | |
| 574 | map = new HashMap<>(); | |
| 575 | } | |
| 569 | 576 | |
| 570 | 577 | return map; |
| 30 | 30 | import static com.keenwrite.io.FileType.UNKNOWN; |
| 31 | 31 | import static com.keenwrite.io.MediaType.TEXT_PROPERTIES; |
| 32 | import static com.keenwrite.io.SysFile.toFile; | |
| 32 | 33 | import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate; |
| 33 | 34 | |
| ... | ||
| 62 | 63 | final var predicate = createFileTypePredicate( patterns ); |
| 63 | 64 | |
| 64 | if( predicate.test( path.toFile() ) ) { | |
| 65 | if( predicate.test( toFile( path ) ) ) { | |
| 65 | 66 | // Remove the EXTENSIONS_PREFIX to get the file name extension mapped |
| 66 | 67 | // to a standard name (as defined in the settings.properties file). |
| ... | ||
| 134 | 135 | final var dir = cacheDir.get(); |
| 135 | 136 | |
| 136 | return (dir == null ? USER_DATA_DIR.toFile() : dir).toPath(); | |
| 137 | return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath(); | |
| 137 | 138 | }; |
| 138 | 139 | } |
| ... | ||
| 160 | 161 | public void setFontDir( final Supplier<File> fontDir ) { |
| 161 | 162 | assert fontDir != null; |
| 163 | ||
| 162 | 164 | mFontDir = () -> { |
| 163 | 165 | final var dir = fontDir.get(); |
| ... | ||
| 199 | 201 | } |
| 200 | 202 | |
| 203 | /** | |
| 204 | * Sets metadata to use in the document header. These are made available | |
| 205 | * to the typesetting engine as {@code \documentvariable} values. | |
| 206 | * | |
| 207 | * @param metadata The key/value pairs to publish as document metadata. | |
| 208 | */ | |
| 201 | 209 | public void setMetadata( final Supplier<Map<String, String>> metadata ) { |
| 202 | 210 | assert metadata != null; |
| 203 | 211 | mMetadata = metadata.get() == null ? HashMap::new : metadata; |
| 212 | } | |
| 213 | ||
| 214 | /** | |
| 215 | * Sets document variables to use when building the document. These | |
| 216 | * variables will override existing key/value pairs, or be added as | |
| 217 | * new key/value pairs if not already defined. This allows users to | |
| 218 | * inject variables into the document from the command-line, allowing | |
| 219 | * for dynamic assignment of in-text values when building documents. | |
| 220 | * | |
| 221 | * @param overrides The key/value pairs to add (or override) as variables. | |
| 222 | */ | |
| 223 | public void setOverrides( final Supplier<Map<String, String>> overrides ) { | |
| 224 | assert overrides != null; | |
| 225 | assert mDefinitions != null; | |
| 226 | assert mDefinitions.get() != null; | |
| 227 | ||
| 228 | final var map = overrides.get(); | |
| 229 | ||
| 230 | if( map != null ) { | |
| 231 | mDefinitions.get().putAll( map ); | |
| 232 | } | |
| 204 | 233 | } |
| 205 | 234 | |
| 12 | 12 | import java.io.FileNotFoundException; |
| 13 | 13 | import java.nio.file.Path; |
| 14 | import java.util.LinkedHashMap; | |
| 15 | import java.util.List; | |
| 16 | import java.util.Locale; | |
| 17 | import java.util.Map; | |
| 14 | import java.util.*; | |
| 18 | 15 | |
| 19 | 16 | import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; |
| 20 | 17 | import static com.keenwrite.dom.DocumentParser.*; |
| 21 | 18 | import static com.keenwrite.events.StatusEvent.clue; |
| 19 | import static com.keenwrite.io.SysFile.toFile; | |
| 22 | 20 | import static com.keenwrite.io.downloads.DownloadManager.open; |
| 23 | 21 | import static com.keenwrite.util.ProtocolScheme.getProtocol; |
| ... | ||
| 132 | 130 | private Map<String, String> createMetaDataMap( final Document doc ) { |
| 133 | 131 | final var result = new LinkedHashMap<String, String>(); |
| 134 | final var metadata = getMetadata(); | |
| 135 | 132 | final var map = mContext.getInterpolatedDefinitions(); |
| 133 | final var metadata = getMetadata(); | |
| 136 | 134 | |
| 137 | 135 | metadata.forEach( |
| ... | ||
| 157 | 155 | */ |
| 158 | 156 | private Map<String, String> getMetadata() { |
| 159 | return mContext.getMetadata(); | |
| 157 | final var result = mContext.getMetadata(); | |
| 158 | return result == null ? new HashMap<>() : result; | |
| 160 | 159 | } |
| 161 | 160 | |
| ... | ||
| 186 | 185 | // Preserve image files if auto-remove is turned off. |
| 187 | 186 | if( autoRemove() ) { |
| 188 | imageFile.toFile().deleteOnExit(); | |
| 187 | toFile( imageFile ).deleteOnExit(); | |
| 189 | 188 | } |
| 190 | 189 | |
| ... | ||
| 214 | 213 | imageFile = imagePath.resolve( filename ); |
| 215 | 214 | |
| 216 | if( imageFile.toFile().exists() ) { | |
| 215 | if( toFile( imageFile ).exists() ) { | |
| 217 | 216 | found = true; |
| 218 | 217 | break; |
| 219 | 218 | } |
| 220 | 219 | } |
| 221 | 220 | |
| 222 | 221 | if( !found ) { |
| 223 | 222 | imagePath = getDocumentDir(); |
| 224 | 223 | imageFile = imagePath.resolve( src ); |
| 225 | 224 | |
| 226 | if( !imageFile.toFile().exists() ) { | |
| 225 | if( !toFile( imageFile ).exists() ) { | |
| 227 | 226 | final var filename = imageFile.toString(); |
| 228 | 227 | clue( "Main.status.image.xhtml.image.missing", filename ); |
| 17 | 17 | |
| 18 | 18 | import static com.keenwrite.events.StatusEvent.clue; |
| 19 | import static com.keenwrite.io.SysFile.toFile; | |
| 19 | 20 | import static com.keenwrite.util.ProtocolScheme.getProtocol; |
| 20 | 21 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; |
| ... | ||
| 115 | 116 | // If the image can be found relative to the base directory, then |
| 116 | 117 | // use the link as is when resolving the path. |
| 117 | return readable( fqfn.toFile() ) | |
| 118 | return readable( toFile( fqfn ) ) | |
| 118 | 119 | ? valid( link, url ) |
| 119 | 120 | : resolveExtensionlessImageFile( link, node, url ); |
| ... | ||
| 145 | 146 | final var baseImagesDir = baseDir.resolve( imagesDir ); |
| 146 | 147 | final var imagePath = baseImagesDir.resolve( url ); |
| 147 | final var file = resolveImageExtension( imagePath.toFile() ); | |
| 148 | final var file = resolveImageExtension( toFile( imagePath ) ); | |
| 148 | 149 | |
| 149 | 150 | if( file.isPresent() ) { |
| 68 | 68 | |
| 69 | 69 | if( !bootstrap.isBlank() ) { |
| 70 | final var map = new HashMap<String, String>( definitions.size() + 1 ); | |
| 70 | final Map<String, String> map; | |
| 71 | 71 | |
| 72 | definitions.forEach( | |
| 73 | ( k, v ) -> map.put( KEY_OPERATOR.apply( k ), escape( v ) ) | |
| 74 | ); | |
| 72 | if( definitions == null ) { | |
| 73 | map = new HashMap<>(); | |
| 74 | } | |
| 75 | else { | |
| 76 | map = new HashMap<>( definitions.size() + 1 ); | |
| 77 | definitions.forEach( | |
| 78 | ( k, v ) -> map.put( KEY_OPERATOR.apply( k ), escape( v ) ) | |
| 79 | ); | |
| 80 | } | |
| 81 | ||
| 75 | 82 | map.put( |
| 76 | 83 | KEY_OPERATOR.apply( "application.r.working.directory" ), |
| 10 | 10 | import org.ahocorasick.trie.Trie; |
| 11 | 11 | |
| 12 | import java.util.ArrayList; | |
| 13 | 12 | import java.util.List; |
| 14 | 13 |
| 17 | 17 | import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY; |
| 18 | 18 | import static com.keenwrite.events.StatusEvent.clue; |
| 19 | import static com.keenwrite.io.SysFile.toFile; | |
| 19 | 20 | import static java.lang.ProcessBuilder.Redirect.DISCARD; |
| 20 | 21 | import static java.nio.file.Files.*; |
| ... | ||
| 108 | 109 | final var stdout = new CircularQueue<String>( 150 ); |
| 109 | 110 | final var builder = new ProcessBuilder( mArgs ); |
| 110 | builder.directory( mDirectory.toFile() ); | |
| 111 | builder.directory( toFile( mDirectory ) ); | |
| 111 | 112 | builder.environment().put( "TEXMFCACHE", getCacheDir().toString() ); |
| 112 | 113 | |
| ... | ||
| 203 | 204 | private java.io.File getCacheDir() { |
| 204 | 205 | final var cache = Path.of( TEMPORARY_DIRECTORY, "luatex-cache" ); |
| 205 | return cache.toFile(); | |
| 206 | return toFile( cache ); | |
| 206 | 207 | } |
| 207 | 208 | |
| 13 | 13 | import static com.keenwrite.Bootstrap.CONTAINER_VERSION; |
| 14 | 14 | import static com.keenwrite.events.StatusEvent.clue; |
| 15 | import static com.keenwrite.io.SysFile.toFile; | |
| 15 | 16 | import static java.lang.String.format; |
| 16 | 17 | import static java.lang.String.join; |
| ... | ||
| 49 | 50 | public static boolean canRun() { |
| 50 | 51 | try { |
| 51 | return getExecutable().toFile().isFile(); | |
| 52 | return toFile( getExecutable() ).isFile(); | |
| 52 | 53 | } catch( final Exception ex ) { |
| 53 | 54 | clue( "Wizard.container.executable.run.error", ex ); |
| ... | ||
| 151 | 152 | assert guestDir != null; |
| 152 | 153 | assert !guestDir.isBlank(); |
| 153 | assert hostDir.toFile().isDirectory(); | |
| 154 | assert toFile( hostDir ).isDirectory(); | |
| 154 | 155 | |
| 155 | 156 | mMountPoints.add( |
| ... | ||
| 217 | 218 | private static ProcessBuilder processBuilder( |
| 218 | 219 | final Path path, final String... s ) { |
| 219 | return processBuilder( path.toFile(), s ); | |
| 220 | return processBuilder( toFile( path ), s ); | |
| 220 | 221 | } |
| 221 | 222 | |
| 16 | 16 | import static com.keenwrite.Messages.getUri; |
| 17 | 17 | import static com.keenwrite.events.StatusEvent.clue; |
| 18 | import static com.keenwrite.io.SysFile.toFile; | |
| 18 | 19 | |
| 19 | 20 | /** |
| ... | ||
| 32 | 33 | mFilename = toFilename( mUri ); |
| 33 | 34 | final var directory = USER_DATA_DIR; |
| 34 | mTarget = directory.resolve( mFilename ).toFile(); | |
| 35 | mTarget = toFile( directory.resolve( mFilename ) ); | |
| 35 | 36 | final var source = labelf( getPrefix() + ".paths", mFilename, directory ); |
| 36 | 37 | mStatus = labelf( getPrefix() + STATUS + ".progress", 0, 0 ); |
| 27 | 27 | import static com.keenwrite.Messages.get; |
| 28 | 28 | import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG; |
| 29 | import static com.keenwrite.io.SysFile.toFile; | |
| 29 | 30 | import static java.lang.System.lineSeparator; |
| 30 | 31 | import static javafx.animation.Interpolator.LINEAR; |
| ... | ||
| 307 | 308 | |
| 308 | 309 | static String toFilename( final URI uri ) { |
| 309 | return Paths.get( uri.getPath() ).toFile().getName(); | |
| 310 | return toFile( Paths.get( uri.getPath() ) ).getName(); | |
| 310 | 311 | } |
| 311 | 312 | } |
| 13 | 13 | import static com.keenwrite.Messages.get; |
| 14 | 14 | import static com.keenwrite.events.StatusEvent.clue; |
| 15 | import static com.keenwrite.io.SysFile.toFile; | |
| 15 | 16 | import static com.keenwrite.preferences.AppKeys.KEY_TYPESET_CONTEXT_THEMES_PATH; |
| 16 | 17 | |
| ... | ||
| 56 | 57 | |
| 57 | 58 | // Replace the default themes directory with the downloaded version. |
| 58 | final var root = Zip.root( target.toPath() ).toFile(); | |
| 59 | final var root = toFile( Zip.root( target.toPath() ) ); | |
| 59 | 60 | |
| 60 | 61 | // Make sure the typesetter will know where to find the themes. |
| 44 | 44 | import java.util.List; |
| 45 | 45 | |
| 46 | import static com.keenwrite.io.SysFile.toFile; | |
| 46 | 47 | import static com.keenwrite.ui.fonts.IconFactory.createGraphic; |
| 47 | 48 | import static org.controlsfx.glyphfont.FontAwesome.Glyph.FILE_ALT; |
| ... | ||
| 87 | 88 | fileChooser.getExtensionFilters() |
| 88 | 89 | .add( new ExtensionFilter( Messages.get( |
| 89 | "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) ); | |
| 90 | "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) ); | |
| 90 | 91 | fileChooser.setInitialDirectory( getInitialDirectory() ); |
| 91 | 92 | var result = fileChooser.showOpenDialog( getScene().getWindow() ); |
| 92 | 93 | if( result != null ) { |
| 93 | 94 | updateUrl( result ); |
| 94 | 95 | } |
| 95 | 96 | } |
| 96 | 97 | |
| 97 | 98 | private File getInitialDirectory() { |
| 98 | 99 | //TODO build initial directory based on current value of 'url' property |
| 99 | return getBasePath().toFile(); | |
| 100 | return toFile( getBasePath() ); | |
| 100 | 101 | } |
| 101 | 102 | |
| 38 | 38 | import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE; |
| 39 | 39 | import static com.keenwrite.events.StatusEvent.clue; |
| 40 | import static com.keenwrite.util.FileWalker.walk; | |
| 41 | import static java.lang.Math.max; | |
| 42 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 43 | import static javafx.application.Platform.runLater; | |
| 44 | import static javafx.geometry.Pos.CENTER; | |
| 45 | import static javafx.scene.control.ButtonType.OK; | |
| 46 | import static org.apache.commons.lang3.StringUtils.abbreviate; | |
| 47 | ||
| 48 | /** | |
| 49 | * Provides controls for exporting to PDF, such as selecting a theme and | |
| 50 | * creating a subset of chapter numbers. | |
| 51 | */ | |
| 52 | public final class ExportDialog extends AbstractDialog<ExportSettings> { | |
| 53 | private record Theme( Path path, String name ) implements Comparable<Theme> { | |
| 54 | /** | |
| 55 | * Answers whether the given theme directory name matches the theme name | |
| 56 | * that the user selected. | |
| 57 | * | |
| 58 | * @param themeDir The user-selected directory to compare with the | |
| 59 | * corresponding path of this {@link Theme}. | |
| 60 | * @return {@code true} if the given directory matches the ending portion | |
| 61 | * of the {@link Path} associated with this {@link Theme} instance. | |
| 62 | */ | |
| 63 | public boolean matches( final String themeDir ) { | |
| 64 | final var f = SysFile.getFileName( path() ); | |
| 65 | ||
| 66 | return f.equalsIgnoreCase( Diacritics.remove( themeDir ) ); | |
| 67 | } | |
| 68 | ||
| 69 | /** | |
| 70 | * Returns the theme's display name. | |
| 71 | * | |
| 72 | * @return The name of the theme presented to users. | |
| 73 | */ | |
| 74 | @Override | |
| 75 | public String toString() { | |
| 76 | return abbreviate( name(), THEME_NAME_LENGTH ); | |
| 77 | } | |
| 78 | ||
| 79 | @Override | |
| 80 | public int compareTo( final Theme o ) { | |
| 81 | assert o != null; | |
| 82 | ||
| 83 | return name().compareTo( o.name() ); | |
| 84 | } | |
| 85 | } | |
| 86 | ||
| 87 | private final File mThemes; | |
| 88 | private final ExportSettings mSettings; | |
| 89 | private GridPane mPane; | |
| 90 | private ComboBox<Theme> mComboBox; | |
| 91 | private TextField mChapters; | |
| 92 | private final boolean mMissingThemes; | |
| 93 | ||
| 94 | /** | |
| 95 | * Construction must use static method to allow caching themes in the | |
| 96 | * future, if needed. | |
| 97 | */ | |
| 98 | private ExportDialog( | |
| 99 | final Window owner, | |
| 100 | final File themesDir, | |
| 101 | final ExportSettings settings, | |
| 102 | final boolean multiple | |
| 103 | ) { | |
| 104 | super( owner, get( "Dialog.typesetting.settings.title" ) ); | |
| 105 | ||
| 106 | assert themesDir != null; | |
| 107 | assert settings != null; | |
| 108 | ||
| 109 | mThemes = themesDir; | |
| 110 | mSettings = settings; | |
| 111 | ||
| 112 | setResultConverter( button -> button == OK ? settings : null ); | |
| 113 | ||
| 114 | final var themes = readThemes( themesDir ); | |
| 115 | ||
| 116 | mMissingThemes = themes.isEmpty(); | |
| 117 | ||
| 118 | // Typesetting installation has been corrupted. This is probably due | |
| 119 | // to the user's settings file gone missing. Rather than force users | |
| 120 | // to find the "themes" directory location, re-install the typesetter, | |
| 121 | if( mMissingThemes ) { | |
| 122 | clue( "Dialog.typesetting.settings.themes.missing", | |
| 123 | themesDir.getAbsolutePath() ); | |
| 124 | ExportFailedEvent.fire(); | |
| 125 | return; | |
| 126 | } | |
| 127 | ||
| 128 | final var previousTheme = mSettings.themeProperty().get(); | |
| 129 | ||
| 130 | initComboBox( mComboBox, previousTheme, themes ); | |
| 131 | ||
| 132 | mPane.add( createLabel( "Dialog.typesetting.settings.theme" ), 0, 1 ); | |
| 133 | mPane.add( mComboBox, 1, 1 ); | |
| 134 | ||
| 135 | var title = "Dialog.typesetting.settings.header."; | |
| 136 | final var focusNode = new AtomicReference<Node>( mComboBox ); | |
| 137 | ||
| 138 | if( multiple ) { | |
| 139 | mPane.add( createLabel( "Dialog.typesetting.settings.chapters" ), 0, 2 ); | |
| 140 | mPane.add( mChapters, 1, 2 ); | |
| 141 | ||
| 142 | focusNode.set( mChapters ); | |
| 143 | title += "multiple"; | |
| 144 | } | |
| 145 | else { | |
| 146 | title += "single"; | |
| 147 | } | |
| 148 | ||
| 149 | // Remember the chapter range regardless of text field visibility. | |
| 150 | mChapters.textProperty().bindBidirectional( mSettings.chaptersProperty() ); | |
| 151 | ||
| 152 | setHeaderText( get( title ) ); | |
| 153 | ||
| 154 | final var dialogPane = getDialogPane(); | |
| 155 | dialogPane.setContent( mPane ); | |
| 156 | ||
| 157 | runLater( () -> focusNode.get().requestFocus() ); | |
| 158 | } | |
| 159 | ||
| 160 | /** | |
| 161 | * Prompts a user to select a theme, answering {@code false} if no theme | |
| 162 | * was selected. The themes must be on the native file system; using the | |
| 163 | * {@link FileWalker} is a little more optimal than {@link ResourceWalker}. | |
| 164 | * | |
| 165 | * @param owner The parent {@link Window} responsible for the dialog. | |
| 166 | * @param themes Theme directory root. | |
| 167 | * @param settings Configuration preferences to use when exporting. | |
| 168 | * @param multiple Pass {@code true} to input a chapter number subset. | |
| 169 | * @return {@code true} if the user accepted or selected a theme. | |
| 170 | */ | |
| 171 | public static boolean choose( | |
| 172 | final Window owner, | |
| 173 | final File themes, | |
| 174 | final ExportSettings settings, | |
| 175 | final boolean multiple | |
| 176 | ) { | |
| 177 | assert themes != null; | |
| 178 | assert settings != null; | |
| 179 | ||
| 180 | return new ExportDialog( owner, themes, settings, multiple ).pick(); | |
| 181 | } | |
| 182 | ||
| 183 | /** | |
| 184 | * @return {@code true} if the user accepted or selected a theme. | |
| 185 | * @see #choose(Window, File, ExportSettings, boolean) | |
| 186 | */ | |
| 187 | private boolean pick() { | |
| 188 | try { | |
| 189 | if( !mMissingThemes ) { | |
| 190 | final var result = showAndWait(); | |
| 191 | ||
| 192 | // The result will only be set if the OK button is pressed. | |
| 193 | if( result.isPresent() ) { | |
| 194 | final var theme = mComboBox.getSelectionModel().getSelectedItem(); | |
| 195 | final var path = theme.path(); | |
| 196 | final var filename = SysFile.getFileName( path.getFileName() ); | |
| 197 | ||
| 198 | mSettings.themeProperty().setValue( filename ); | |
| 199 | ||
| 200 | return true; | |
| 201 | } | |
| 202 | } | |
| 203 | } catch( final Exception ex ) { | |
| 204 | clue( get( "Main.status.error.theme.missing", mThemes ), ex ); | |
| 205 | } | |
| 206 | ||
| 207 | return false; | |
| 208 | } | |
| 209 | ||
| 210 | @Override | |
| 211 | protected void initComponents() { | |
| 212 | initIcon(); | |
| 213 | setResizable( true ); | |
| 214 | ||
| 215 | mPane = createContentPane(); | |
| 216 | mComboBox = createComboBox(); | |
| 217 | mComboBox.setOnKeyPressed( event -> { | |
| 218 | // When the user presses the down arrow, open the drop-down. This | |
| 219 | // prevents navigating to the cancel button. | |
| 220 | if( event.getCode() == KeyCode.DOWN && !mComboBox.isShowing() ) { | |
| 221 | mComboBox.show(); | |
| 222 | event.consume(); | |
| 223 | } | |
| 224 | } ); | |
| 225 | ||
| 226 | mChapters = createNumericTextField(); | |
| 227 | } | |
| 228 | ||
| 229 | private void initIcon() { | |
| 230 | setGraphic( ICON_DIALOG_NODE ); | |
| 231 | setStageGraphic( ICON_DIALOG ); | |
| 232 | } | |
| 233 | ||
| 234 | @SuppressWarnings( "SameParameterValue" ) | |
| 235 | private void setStageGraphic( final Image icon ) { | |
| 236 | if( getDialogPane().getScene().getWindow() instanceof final Stage stage ) { | |
| 237 | stage.getIcons().add( icon ); | |
| 238 | } | |
| 239 | } | |
| 240 | ||
| 241 | private void initComboBox( | |
| 242 | final ComboBox<Theme> comboBox, | |
| 243 | final String previousTheme, | |
| 244 | final List<Theme> choices | |
| 245 | ) { | |
| 246 | assert comboBox != null; | |
| 247 | assert previousTheme != null; | |
| 248 | assert choices != null; | |
| 249 | ||
| 250 | final var items = comboBox.getItems(); | |
| 251 | items.clear(); | |
| 252 | items.addAll( choices ); | |
| 253 | ||
| 254 | // Set the selected item to user's settings value. | |
| 255 | for( final var choice : choices ) { | |
| 256 | if( choice.matches( previousTheme ) ) { | |
| 257 | comboBox.getSelectionModel().select( | |
| 258 | items.get( max( items.indexOf( choice ), 0 ) ) | |
| 259 | ); | |
| 260 | ||
| 261 | break; | |
| 262 | } | |
| 263 | } | |
| 264 | } | |
| 265 | ||
| 266 | private List<Theme> readThemes( final File themesDir ) { | |
| 267 | try { | |
| 268 | // List themes in alphabetical order (human-readable by directory name). | |
| 269 | final var choices = new LinkedList<Theme>(); | |
| 270 | ||
| 271 | // Populate the choices with themes detected on the system. | |
| 272 | walk( themesDir.toPath(), "**/theme.properties", path -> { | |
| 273 | try { | |
| 274 | final var themeName = readThemeName( path ); | |
| 275 | final var themePath = path.getParent(); | |
| 276 | choices.add( new Theme( themePath, themeName ) ); | |
| 277 | } catch( final Exception ex ) { | |
| 278 | clue( "Main.status.error.theme.name", path ); | |
| 279 | } | |
| 280 | } ); | |
| 281 | ||
| 282 | Collections.sort( choices ); | |
| 283 | ||
| 284 | return choices; | |
| 285 | } catch( final Exception ex ) { | |
| 286 | clue( ex ); | |
| 287 | } | |
| 288 | ||
| 289 | return Collections.emptyList(); | |
| 290 | } | |
| 291 | ||
| 292 | private ComboBox<Theme> createComboBox() { | |
| 293 | return new ComboBox<>(); | |
| 294 | } | |
| 295 | ||
| 296 | private GridPane createContentPane() { | |
| 297 | final var grid = new GridPane(); | |
| 298 | ||
| 299 | grid.setAlignment( CENTER ); | |
| 300 | grid.setHgap( UI_CONTROL_SPACING ); | |
| 301 | grid.setVgap( UI_CONTROL_SPACING ); | |
| 302 | grid.setPadding( new Insets( 25, 25, 25, 25 ) ); | |
| 303 | ||
| 304 | return grid; | |
| 305 | } | |
| 306 | ||
| 307 | /** | |
| 308 | * Creates an input field that only accepts whole numbers. This allows users | |
| 309 | * to enter in chapter ranges such as: <code>1-5, 7, 9-10</code>. | |
| 310 | * | |
| 311 | * @return A {@link TextField} that censors non-conforming characters. | |
| 312 | */ | |
| 313 | private TextField createNumericTextField() { | |
| 314 | final var textField = new TextField(); | |
| 315 | ||
| 316 | textField.textProperty().addListener( | |
| 317 | ( c, o, n ) -> textField.setText( RangeValidator.normalize( n ) ) | |
| 318 | ); | |
| 319 | ||
| 320 | return textField; | |
| 321 | } | |
| 322 | ||
| 323 | private Label createLabel( final String key ) { | |
| 324 | final var label = new Label( get( key ) + ":" ); | |
| 325 | final var font = label.getFont(); | |
| 326 | final var upscale = new Font( font.getName(), 14 ); | |
| 327 | ||
| 328 | label.setFont( upscale ); | |
| 329 | ||
| 330 | return label; | |
| 331 | } | |
| 332 | ||
| 333 | /** | |
| 334 | * Returns the theme's human-friendly name from a file conforming to | |
| 335 | * {@link Properties}. | |
| 336 | * | |
| 337 | * @param file A fully qualified file name readable using {@link Properties}. | |
| 338 | * @return The human-friendly theme name. | |
| 339 | * @throws IOException The {@link Properties} file cannot be read. | |
| 340 | * @throws NullPointerException The name field is not defined. | |
| 341 | */ | |
| 342 | private String readThemeName( final Path file ) throws Exception { | |
| 343 | return read( file ).get( "name" ).toString(); | |
| 344 | } | |
| 345 | ||
| 346 | /** | |
| 347 | * Reads an instance of {@link Properties} from the given {@link Path} using | |
| 348 | * {@link StandardCharsets#UTF_8} encoding. | |
| 349 | * | |
| 350 | * @param path The fully qualified path to the file. | |
| 351 | * @return The path to the file to read. | |
| 352 | * @throws IOException Could not open the file for reading. | |
| 353 | */ | |
| 354 | private Properties read( final Path path ) throws IOException { | |
| 355 | final var properties = new Properties(); | |
| 356 | ||
| 357 | try( | |
| 358 | final var f = new FileInputStream( path.toFile() ); | |
| 40 | import static com.keenwrite.io.SysFile.toFile; | |
| 41 | import static com.keenwrite.util.FileWalker.walk; | |
| 42 | import static java.lang.Math.max; | |
| 43 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 44 | import static javafx.application.Platform.runLater; | |
| 45 | import static javafx.geometry.Pos.CENTER; | |
| 46 | import static javafx.scene.control.ButtonType.OK; | |
| 47 | import static org.apache.commons.lang3.StringUtils.abbreviate; | |
| 48 | ||
| 49 | /** | |
| 50 | * Provides controls for exporting to PDF, such as selecting a theme and | |
| 51 | * creating a subset of chapter numbers. | |
| 52 | */ | |
| 53 | public final class ExportDialog extends AbstractDialog<ExportSettings> { | |
| 54 | private record Theme( Path path, String name ) implements Comparable<Theme> { | |
| 55 | /** | |
| 56 | * Answers whether the given theme directory name matches the theme name | |
| 57 | * that the user selected. | |
| 58 | * | |
| 59 | * @param themeDir The user-selected directory to compare with the | |
| 60 | * corresponding path of this {@link Theme}. | |
| 61 | * @return {@code true} if the given directory matches the ending portion | |
| 62 | * of the {@link Path} associated with this {@link Theme} instance. | |
| 63 | */ | |
| 64 | public boolean matches( final String themeDir ) { | |
| 65 | final var f = SysFile.getFileName( path() ); | |
| 66 | ||
| 67 | return f.equalsIgnoreCase( Diacritics.remove( themeDir ) ); | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Returns the theme's display name. | |
| 72 | * | |
| 73 | * @return The name of the theme presented to users. | |
| 74 | */ | |
| 75 | @Override | |
| 76 | public String toString() { | |
| 77 | return abbreviate( name(), THEME_NAME_LENGTH ); | |
| 78 | } | |
| 79 | ||
| 80 | @Override | |
| 81 | public int compareTo( final Theme o ) { | |
| 82 | assert o != null; | |
| 83 | ||
| 84 | return name().compareTo( o.name() ); | |
| 85 | } | |
| 86 | } | |
| 87 | ||
| 88 | private final File mThemes; | |
| 89 | private final ExportSettings mSettings; | |
| 90 | private GridPane mPane; | |
| 91 | private ComboBox<Theme> mComboBox; | |
| 92 | private TextField mChapters; | |
| 93 | private final boolean mMissingThemes; | |
| 94 | ||
| 95 | /** | |
| 96 | * Construction must use static method to allow caching themes in the | |
| 97 | * future, if needed. | |
| 98 | */ | |
| 99 | private ExportDialog( | |
| 100 | final Window owner, | |
| 101 | final File themesDir, | |
| 102 | final ExportSettings settings, | |
| 103 | final boolean multiple | |
| 104 | ) { | |
| 105 | super( owner, get( "Dialog.typesetting.settings.title" ) ); | |
| 106 | ||
| 107 | assert themesDir != null; | |
| 108 | assert settings != null; | |
| 109 | ||
| 110 | mThemes = themesDir; | |
| 111 | mSettings = settings; | |
| 112 | ||
| 113 | setResultConverter( button -> button == OK ? settings : null ); | |
| 114 | ||
| 115 | final var themes = readThemes( themesDir ); | |
| 116 | ||
| 117 | mMissingThemes = themes.isEmpty(); | |
| 118 | ||
| 119 | // Typesetting installation has been corrupted. This is probably due | |
| 120 | // to the user's settings file gone missing. Rather than force users | |
| 121 | // to find the "themes" directory location, re-install the typesetter, | |
| 122 | if( mMissingThemes ) { | |
| 123 | clue( "Dialog.typesetting.settings.themes.missing", | |
| 124 | themesDir.getAbsolutePath() ); | |
| 125 | ExportFailedEvent.fire(); | |
| 126 | return; | |
| 127 | } | |
| 128 | ||
| 129 | final var previousTheme = mSettings.themeProperty().get(); | |
| 130 | ||
| 131 | initComboBox( mComboBox, previousTheme, themes ); | |
| 132 | ||
| 133 | mPane.add( createLabel( "Dialog.typesetting.settings.theme" ), 0, 1 ); | |
| 134 | mPane.add( mComboBox, 1, 1 ); | |
| 135 | ||
| 136 | var title = "Dialog.typesetting.settings.header."; | |
| 137 | final var focusNode = new AtomicReference<Node>( mComboBox ); | |
| 138 | ||
| 139 | if( multiple ) { | |
| 140 | mPane.add( createLabel( "Dialog.typesetting.settings.chapters" ), 0, 2 ); | |
| 141 | mPane.add( mChapters, 1, 2 ); | |
| 142 | ||
| 143 | focusNode.set( mChapters ); | |
| 144 | title += "multiple"; | |
| 145 | } | |
| 146 | else { | |
| 147 | title += "single"; | |
| 148 | } | |
| 149 | ||
| 150 | // Remember the chapter range regardless of text field visibility. | |
| 151 | mChapters.textProperty().bindBidirectional( mSettings.chaptersProperty() ); | |
| 152 | ||
| 153 | setHeaderText( get( title ) ); | |
| 154 | ||
| 155 | final var dialogPane = getDialogPane(); | |
| 156 | dialogPane.setContent( mPane ); | |
| 157 | ||
| 158 | runLater( () -> focusNode.get().requestFocus() ); | |
| 159 | } | |
| 160 | ||
| 161 | /** | |
| 162 | * Prompts a user to select a theme, answering {@code false} if no theme | |
| 163 | * was selected. The themes must be on the native file system; using the | |
| 164 | * {@link FileWalker} is a little more optimal than {@link ResourceWalker}. | |
| 165 | * | |
| 166 | * @param owner The parent {@link Window} responsible for the dialog. | |
| 167 | * @param themes Theme directory root. | |
| 168 | * @param settings Configuration preferences to use when exporting. | |
| 169 | * @param multiple Pass {@code true} to input a chapter number subset. | |
| 170 | * @return {@code true} if the user accepted or selected a theme. | |
| 171 | */ | |
| 172 | public static boolean choose( | |
| 173 | final Window owner, | |
| 174 | final File themes, | |
| 175 | final ExportSettings settings, | |
| 176 | final boolean multiple | |
| 177 | ) { | |
| 178 | assert themes != null; | |
| 179 | assert settings != null; | |
| 180 | ||
| 181 | return new ExportDialog( owner, themes, settings, multiple ).pick(); | |
| 182 | } | |
| 183 | ||
| 184 | /** | |
| 185 | * @return {@code true} if the user accepted or selected a theme. | |
| 186 | * @see #choose(Window, File, ExportSettings, boolean) | |
| 187 | */ | |
| 188 | private boolean pick() { | |
| 189 | try { | |
| 190 | if( !mMissingThemes ) { | |
| 191 | final var result = showAndWait(); | |
| 192 | ||
| 193 | // The result will only be set if the OK button is pressed. | |
| 194 | if( result.isPresent() ) { | |
| 195 | final var theme = mComboBox.getSelectionModel().getSelectedItem(); | |
| 196 | final var path = theme.path(); | |
| 197 | final var filename = SysFile.getFileName( path.getFileName() ); | |
| 198 | ||
| 199 | mSettings.themeProperty().setValue( filename ); | |
| 200 | ||
| 201 | return true; | |
| 202 | } | |
| 203 | } | |
| 204 | } catch( final Exception ex ) { | |
| 205 | clue( get( "Main.status.error.theme.missing", mThemes ), ex ); | |
| 206 | } | |
| 207 | ||
| 208 | return false; | |
| 209 | } | |
| 210 | ||
| 211 | @Override | |
| 212 | protected void initComponents() { | |
| 213 | initIcon(); | |
| 214 | setResizable( true ); | |
| 215 | ||
| 216 | mPane = createContentPane(); | |
| 217 | mComboBox = createComboBox(); | |
| 218 | mComboBox.setOnKeyPressed( event -> { | |
| 219 | // When the user presses the down arrow, open the drop-down. This | |
| 220 | // prevents navigating to the cancel button. | |
| 221 | if( event.getCode() == KeyCode.DOWN && !mComboBox.isShowing() ) { | |
| 222 | mComboBox.show(); | |
| 223 | event.consume(); | |
| 224 | } | |
| 225 | } ); | |
| 226 | ||
| 227 | mChapters = createNumericTextField(); | |
| 228 | } | |
| 229 | ||
| 230 | private void initIcon() { | |
| 231 | setGraphic( ICON_DIALOG_NODE ); | |
| 232 | setStageGraphic( ICON_DIALOG ); | |
| 233 | } | |
| 234 | ||
| 235 | @SuppressWarnings( "SameParameterValue" ) | |
| 236 | private void setStageGraphic( final Image icon ) { | |
| 237 | if( getDialogPane().getScene().getWindow() instanceof final Stage stage ) { | |
| 238 | stage.getIcons().add( icon ); | |
| 239 | } | |
| 240 | } | |
| 241 | ||
| 242 | private void initComboBox( | |
| 243 | final ComboBox<Theme> comboBox, | |
| 244 | final String previousTheme, | |
| 245 | final List<Theme> choices | |
| 246 | ) { | |
| 247 | assert comboBox != null; | |
| 248 | assert previousTheme != null; | |
| 249 | assert choices != null; | |
| 250 | ||
| 251 | final var items = comboBox.getItems(); | |
| 252 | items.clear(); | |
| 253 | items.addAll( choices ); | |
| 254 | ||
| 255 | // Set the selected item to user's settings value. | |
| 256 | for( final var choice : choices ) { | |
| 257 | if( choice.matches( previousTheme ) ) { | |
| 258 | comboBox.getSelectionModel().select( | |
| 259 | items.get( max( items.indexOf( choice ), 0 ) ) | |
| 260 | ); | |
| 261 | ||
| 262 | break; | |
| 263 | } | |
| 264 | } | |
| 265 | } | |
| 266 | ||
| 267 | private List<Theme> readThemes( final File themesDir ) { | |
| 268 | try { | |
| 269 | // List themes in alphabetical order (human-readable by directory name). | |
| 270 | final var choices = new LinkedList<Theme>(); | |
| 271 | ||
| 272 | // Populate the choices with themes detected on the system. | |
| 273 | walk( themesDir.toPath(), "**/theme.properties", path -> { | |
| 274 | try { | |
| 275 | final var themeName = readThemeName( path ); | |
| 276 | final var themePath = path.getParent(); | |
| 277 | choices.add( new Theme( themePath, themeName ) ); | |
| 278 | } catch( final Exception ex ) { | |
| 279 | clue( "Main.status.error.theme.name", path ); | |
| 280 | } | |
| 281 | } ); | |
| 282 | ||
| 283 | Collections.sort( choices ); | |
| 284 | ||
| 285 | return choices; | |
| 286 | } catch( final Exception ex ) { | |
| 287 | clue( ex ); | |
| 288 | } | |
| 289 | ||
| 290 | return Collections.emptyList(); | |
| 291 | } | |
| 292 | ||
| 293 | private ComboBox<Theme> createComboBox() { | |
| 294 | return new ComboBox<>(); | |
| 295 | } | |
| 296 | ||
| 297 | private GridPane createContentPane() { | |
| 298 | final var grid = new GridPane(); | |
| 299 | ||
| 300 | grid.setAlignment( CENTER ); | |
| 301 | grid.setHgap( UI_CONTROL_SPACING ); | |
| 302 | grid.setVgap( UI_CONTROL_SPACING ); | |
| 303 | grid.setPadding( new Insets( 25, 25, 25, 25 ) ); | |
| 304 | ||
| 305 | return grid; | |
| 306 | } | |
| 307 | ||
| 308 | /** | |
| 309 | * Creates an input field that only accepts whole numbers. This allows users | |
| 310 | * to enter in chapter ranges such as: <code>1-5, 7, 9-10</code>. | |
| 311 | * | |
| 312 | * @return A {@link TextField} that censors non-conforming characters. | |
| 313 | */ | |
| 314 | private TextField createNumericTextField() { | |
| 315 | final var textField = new TextField(); | |
| 316 | ||
| 317 | textField.textProperty().addListener( | |
| 318 | ( c, o, n ) -> textField.setText( RangeValidator.normalize( n ) ) | |
| 319 | ); | |
| 320 | ||
| 321 | return textField; | |
| 322 | } | |
| 323 | ||
| 324 | private Label createLabel( final String key ) { | |
| 325 | final var label = new Label( get( key ) + ":" ); | |
| 326 | final var font = label.getFont(); | |
| 327 | final var upscale = new Font( font.getName(), 14 ); | |
| 328 | ||
| 329 | label.setFont( upscale ); | |
| 330 | ||
| 331 | return label; | |
| 332 | } | |
| 333 | ||
| 334 | /** | |
| 335 | * Returns the theme's human-friendly name from a file conforming to | |
| 336 | * {@link Properties}. | |
| 337 | * | |
| 338 | * @param file A fully qualified file name readable using {@link Properties}. | |
| 339 | * @return The human-friendly theme name. | |
| 340 | * @throws IOException The {@link Properties} file cannot be read. | |
| 341 | * @throws NullPointerException The name field is not defined. | |
| 342 | */ | |
| 343 | private String readThemeName( final Path file ) throws Exception { | |
| 344 | return read( file ).get( "name" ).toString(); | |
| 345 | } | |
| 346 | ||
| 347 | /** | |
| 348 | * Reads an instance of {@link Properties} from the given {@link Path} using | |
| 349 | * {@link StandardCharsets#UTF_8} encoding. | |
| 350 | * | |
| 351 | * @param path The fully qualified path to the file. | |
| 352 | * @return The path to the file to read. | |
| 353 | * @throws IOException Could not open the file for reading. | |
| 354 | */ | |
| 355 | private Properties read( final Path path ) throws IOException { | |
| 356 | final var properties = new Properties(); | |
| 357 | ||
| 358 | try( | |
| 359 | final var f = new FileInputStream( toFile( path ) ); | |
| 359 | 360 | final var in = new InputStreamReader( f, UTF_8 ) |
| 360 | 361 | ) { |
| 15 | 15 | import java.util.Optional; |
| 16 | 16 | |
| 17 | import static com.keenwrite.io.SysFile.toFile; | |
| 17 | 18 | import static com.keenwrite.preferences.AppKeys.KEY_UI_RECENT_DIR; |
| 18 | 19 | import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*; |
| ... | ||
| 92 | 93 | @Override |
| 93 | 94 | public void setInitialDirectory( final Path path ) { |
| 94 | assert path != null; | |
| 95 | ||
| 96 | final var file = path.toFile(); | |
| 95 | final var file = toFile( path ); | |
| 97 | 96 | |
| 98 | 97 | mChooser.setInitialDirectory( |
| 26 | 26 | import static com.keenwrite.constants.Constants.UI_CONTROL_SPACING; |
| 27 | 27 | import static com.keenwrite.events.StatusEvent.clue; |
| 28 | import static com.keenwrite.io.SysFile.toFile; | |
| 28 | 29 | import static com.keenwrite.ui.fonts.IconFactory.createFileIcon; |
| 29 | 30 | import static java.nio.file.Files.size; |
| ... | ||
| 170 | 171 | final var filename = entry.nameProperty().get(); |
| 171 | 172 | final var path = Path.of( dir.toString(), filename ); |
| 172 | final var file = path.toFile(); | |
| 173 | final var file = toFile( path ); | |
| 173 | 174 | |
| 174 | 175 | if( file.isFile() ) { |
| 175 | 176 | FileOpenEvent.fire( path.toUri() ); |
| 176 | 177 | } |
| 177 | 178 | else if( file.isDirectory() ) { |
| 178 | mDirectory.set( path.normalize().toFile() ); | |
| 179 | mDirectory.set( toFile( path.normalize() ) ); | |
| 179 | 180 | } |
| 180 | 181 | } |
| ... | ||
| 271 | 272 | SysFile.getFileName( path ), |
| 272 | 273 | size( path ), |
| 273 | ofEpochMilli( path.toFile().lastModified() ) | |
| 274 | ofEpochMilli( toFile( path ).lastModified() ) | |
| 274 | 275 | ); |
| 275 | 276 | } |
| 7 | 7 | import com.keenwrite.ui.actions.Keyboard; |
| 8 | 8 | import com.keenwrite.ui.clipboard.Clipboard; |
| 9 | import com.whitemagicsoftware.wordcount.TokenizerException; | |
| 9 | import com.whitemagicsoftware.keencount.TokenizerException; | |
| 10 | 10 | import javafx.beans.property.IntegerProperty; |
| 11 | 11 | import javafx.beans.property.SimpleIntegerProperty; |
| 2 | 2 | package com.keenwrite.ui.heuristics; |
| 3 | 3 | |
| 4 | import com.whitemagicsoftware.wordcount.Tokenizer; | |
| 5 | import com.whitemagicsoftware.wordcount.TokenizerFactory; | |
| 4 | import com.whitemagicsoftware.keencount.Tokenizer; | |
| 5 | import com.whitemagicsoftware.keencount.TokenizerFactory; | |
| 6 | 6 | |
| 7 | 7 | import java.util.Locale; |
| 78 | 78 | // paragraph, instead. |
| 79 | 79 | if( text.isBlank() ) { |
| 80 | final var paragraphs = editor.getParagraphs().size(); | |
| 80 | final var paragraphs = editor.getParagraphs(); | |
| 81 | final var count = paragraphs == null ? 0 : paragraphs.size(); | |
| 81 | 82 | |
| 82 | paraId = Math.min( paraId + 1, paragraphs - 1 ); | |
| 83 | paraId = Math.min( paraId + 1, count - 1 ); | |
| 83 | 84 | paragraph = editor.getParagraph( paraId ); |
| 84 | 85 | text = paragraph.getText(); |
| 4 | 4 | import org.junit.jupiter.api.Test; |
| 5 | 5 | |
| 6 | import java.io.FileNotFoundException; | |
| 7 | ||
| 8 | import static org.junit.jupiter.api.Assertions.*; | |
| 6 | import static org.junit.jupiter.api.Assertions.assertFalse; | |
| 7 | import static org.junit.jupiter.api.Assertions.assertTrue; | |
| 9 | 8 | |
| 10 | 9 | class UserDataDirTest { |
| 11 | 10 | @Test |
| 12 | void test_Unix_GetAppDirectory_DirectoryExists() | |
| 13 | throws FileNotFoundException { | |
| 11 | void test_Unix_GetAppDirectory_DirectoryExists() { | |
| 14 | 12 | final var path = UserDataDir.getAppPath( "test" ); |
| 15 | 13 | final var file = path.toFile(); |