Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M .gitignore
99
.classpath
1010
.idea
11
count
1112
themes
1213
quotes
M docs/cmd.md
1212
* `-i` -- sets the input file name, must be a full path.
1313
* `-o` -- sets the output file name, can be a relative path.
14
* `-s` -- sets a variable name and value at build time (dynamic data).
1415
1516
## Example usage
A libs/keencount-min.jar
Binary file
M libs/keenquotes.jar
Binary file
D libs/tokenize.jar
Binary file
M src/main/java/com/keenwrite/Bootstrap.java
1212
1313
import static com.keenwrite.events.StatusEvent.clue;
14
import static com.keenwrite.io.SysFile.toFile;
1415
1516
/**
...
8384
8485
    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" ) );
8687
8788
    if( !USER_CACHE_DIR.exists() && !USER_CACHE_DIR.mkdirs() ) {
M src/main/java/com/keenwrite/ExportFormat.java
88
import java.nio.file.Path;
99
10
import static com.keenwrite.io.SysFile.toFile;
1011
import static java.lang.String.format;
1112
import static org.apache.commons.io.FilenameUtils.removeExtension;
...
125126
   */
126127
  public File toExportFilename( final Path path ) {
127
    return toExportFilename( path.toFile() );
128
    return toExportFilename( toFile( path ) );
128129
  }
129130
}
M src/main/java/com/keenwrite/MainPane.java
7474
import static com.keenwrite.io.MediaType.*;
7575
import static com.keenwrite.io.MediaType.TypeName.TEXT;
76
import static com.keenwrite.io.SysFile.toFile;
7677
import static com.keenwrite.preferences.AppKeys.*;
7778
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
...
307308
      else {
308309
        final var parentPath = parent.getAbsolutePath();
309
        eventFile = Path.of( parentPath, eventUri.getPath() ).toFile();
310
        eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) );
310311
      }
311312
    }
M src/main/java/com/keenwrite/cmdline/Arguments.java
191191
192192
  @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(
193201
    names = {"--sigil-opening"},
194202
    description =
...
250258
      .with( Mutator::setDefinitions, () -> definitions )
251259
      .with( Mutator::setMetadata, () -> mMetadata )
260
      .with( Mutator::setOverrides, () -> mOverrides )
252261
      .with( Mutator::setLocale, () -> locale )
253262
      .with( Mutator::setConcatenate, () -> mConcatenate )
M src/main/java/com/keenwrite/cmdline/HeadlessApp.java
4040
  public void handle( final StatusEvent event ) {
4141
    if( !mArgs.quiet() ) {
42
      System.out.println( event );
42
      System.out.println( event.toString() );
43
      System.out.println( event.getProblem() );
4344
    }
4445
  }
4546
4647
  /**
4748
   * Entry point for running the application in headless mode.
4849
   *
4950
   * @param args The parsed command-line arguments.
5051
   */
52
  @SuppressWarnings( "ConfusingMainMethod" )
5153
  public static void main( final Arguments args ) {
5254
    new HeadlessApp( args );
M src/main/java/com/keenwrite/constants/Constants.java
1313
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
1414
import static com.keenwrite.Bootstrap.USER_DATA_DIR;
15
import static com.keenwrite.io.SysFile.toFile;
1516
import static com.keenwrite.preferences.LocaleScripts.withScript;
1617
import static java.io.File.separator;
...
308309
    }
309310
310
    return (fontBase == null
311
    final var base = fontBase == null
311312
      ? USER_DATA_DIR.relativize( fontUser )
312
      : Path.of( fontBase ).resolve( fontUser )).toFile();
313
      : Path.of( fontBase ).resolve( fontUser );
314
315
    return toFile( base );
313316
  }
314317
}
M src/main/java/com/keenwrite/dom/DocumentParser.java
11
package com.keenwrite.dom;
22
3
import com.keenwrite.io.SysFile;
34
import org.w3c.dom.*;
45
import org.xml.sax.InputSource;
...
2324
2425
import static com.keenwrite.events.StatusEvent.clue;
26
import static com.keenwrite.io.SysFile.toFile;
2527
import static java.nio.charset.StandardCharsets.UTF_16;
2628
import static java.nio.charset.StandardCharsets.UTF_8;
...
245247
246248
    final var target = new StreamResult( sOutput );
247
    final var source = sDocumentBuilder.parse( path.toFile() );
249
    final var source = sDocumentBuilder.parse( toFile( path ) );
248250
249251
    transform( source, target );
M src/main/java/com/keenwrite/editors/TextResource.java
1313
import static com.keenwrite.constants.Constants.DEFAULT_CHARSET;
1414
import static com.keenwrite.events.StatusEvent.clue;
15
import static com.keenwrite.io.SysFile.toFile;
1516
import static java.nio.charset.Charset.forName;
1617
import static java.nio.file.Files.readAllBytes;
...
114115
   */
115116
  default Charset open( final Path path ) {
116
    final var file = path.toFile();
117
    final var file = toFile( path );
117118
    Charset encoding = DEFAULT_CHARSET;
118119
M src/main/java/com/keenwrite/io/FileWatchService.java
1313
import java.util.concurrent.ConcurrentHashMap;
1414
15
import static com.keenwrite.io.SysFile.toFile;
1516
import static java.nio.file.FileSystems.getDefault;
1617
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
...
7273
        final var watchable = (Path) watchKey.watchable();
7374
        final var context = (Path) pollEvent.context();
74
        final var file = watchable.resolve( context ).toFile();
75
        final var file = toFile( watchable.resolve( context ) );
7576
7677
        if( mWatched.containsKey( file ) ) {
M src/main/java/com/keenwrite/io/MediaType.java
99
import static com.keenwrite.io.MediaType.TypeName.*;
1010
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 ) );
352353
353354
    if( purge ) {
M src/main/java/com/keenwrite/io/MediaTypeExtension.java
99
1010
import static com.keenwrite.io.MediaType.*;
11
import static com.keenwrite.io.SysFile.toFile;
1112
import static java.util.List.of;
1213
...
103104
104105
  public static MediaType fromPath( final Path path ) {
105
    return fromFile( path.toFile() );
106
    return fromFile( toFile( path ) );
106107
  }
107108
M src/main/java/com/keenwrite/io/SysFile.java
267267
   * deleted or moved.
268268
   *
269
   * @param path The path to verify existence.
269
   * @param path The path to verify existence, may be null.
270270
   * @return The given path, if it exists, otherwise the user's home directory.
271271
   */
272272
  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
  }
274279
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();
276284
  }
277285
M src/main/java/com/keenwrite/io/UserDataDir.java
44
import java.nio.file.Path;
55
6
import static com.keenwrite.io.SysFile.toFile;
67
import static java.lang.System.getProperty;
78
import static java.lang.System.getenv;
...
131132
   */
132133
  private static boolean ensureExists( final Path path ) {
133
    final var file = path.toFile();
134
    final var file = toFile( path );
134135
    return file.exists() || file.mkdirs();
135136
  }
M src/main/java/com/keenwrite/io/Zip.java
1212
import java.util.zip.ZipFile;
1313
14
import static com.keenwrite.io.SysFile.toFile;
1415
import static java.nio.file.Files.createDirectories;
1516
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
...
105106
    final BiConsumer<ZipFile, ZipEntry> consumer )
106107
    throws IOException {
107
    assert zipPath.toFile().isFile();
108
    assert toFile( zipPath ).isFile();
108109
109
    try( final var zipFile = new ZipFile( zipPath.toFile() ) ) {
110
    try( final var zipFile = new ZipFile( toFile( zipPath ) ) ) {
110111
      final var entries = zipFile.entries();
111112
M src/main/java/com/keenwrite/preferences/Workspace.java
562562
  public <K, V> Map<K, V> getMetadata() {
563563
    final var metadata = listsProperty( KEY_DOC_META );
564
    final var map = new HashMap<K, V>( metadata.size() );
564
    final HashMap<K, V> map;
565565
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
    }
569576
570577
    return map;
M src/main/java/com/keenwrite/processors/ProcessorContext.java
3030
import static com.keenwrite.io.FileType.UNKNOWN;
3131
import static com.keenwrite.io.MediaType.TEXT_PROPERTIES;
32
import static com.keenwrite.io.SysFile.toFile;
3233
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
3334
...
6263
      final var predicate = createFileTypePredicate( patterns );
6364
64
      if( predicate.test( path.toFile() ) ) {
65
      if( predicate.test( toFile( path ) ) ) {
6566
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
6667
        // to a standard name (as defined in the settings.properties file).
...
134135
        final var dir = cacheDir.get();
135136
136
        return (dir == null ? USER_DATA_DIR.toFile() : dir).toPath();
137
        return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
137138
      };
138139
    }
...
160161
    public void setFontDir( final Supplier<File> fontDir ) {
161162
      assert fontDir != null;
163
162164
      mFontDir = () -> {
163165
        final var dir = fontDir.get();
...
199201
    }
200202
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
     */
201209
    public void setMetadata( final Supplier<Map<String, String>> metadata ) {
202210
      assert metadata != null;
203211
      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
      }
204233
    }
205234
M src/main/java/com/keenwrite/processors/XhtmlProcessor.java
1212
import java.io.FileNotFoundException;
1313
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.*;
1815
1916
import static com.keenwrite.Bootstrap.APP_TITLE_ABBR;
2017
import static com.keenwrite.dom.DocumentParser.*;
2118
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.io.SysFile.toFile;
2220
import static com.keenwrite.io.downloads.DownloadManager.open;
2321
import static com.keenwrite.util.ProtocolScheme.getProtocol;
...
132130
  private Map<String, String> createMetaDataMap( final Document doc ) {
133131
    final var result = new LinkedHashMap<String, String>();
134
    final var metadata = getMetadata();
135132
    final var map = mContext.getInterpolatedDefinitions();
133
    final var metadata = getMetadata();
136134
137135
    metadata.forEach(
...
157155
   */
158156
  private Map<String, String> getMetadata() {
159
    return mContext.getMetadata();
157
    final var result = mContext.getMetadata();
158
    return result == null ? new HashMap<>() : result;
160159
  }
161160
...
186185
      // Preserve image files if auto-remove is turned off.
187186
      if( autoRemove() ) {
188
        imageFile.toFile().deleteOnExit();
187
        toFile( imageFile ).deleteOnExit();
189188
      }
190189
...
214213
      imageFile = imagePath.resolve( filename );
215214
216
      if( imageFile.toFile().exists() ) {
215
      if( toFile( imageFile ).exists() ) {
217216
        found = true;
218217
        break;
219218
      }
220219
    }
221220
222221
    if( !found ) {
223222
      imagePath = getDocumentDir();
224223
      imageFile = imagePath.resolve( src );
225224
226
      if( !imageFile.toFile().exists() ) {
225
      if( !toFile( imageFile ).exists() ) {
227226
        final var filename = imageFile.toString();
228227
        clue( "Main.status.image.xhtml.image.missing", filename );
M src/main/java/com/keenwrite/processors/markdown/extensions/ImageLinkExtension.java
1717
1818
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.io.SysFile.toFile;
1920
import static com.keenwrite.util.ProtocolScheme.getProtocol;
2021
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
...
115116
      // If the image can be found relative to the base directory, then
116117
      // use the link as is when resolving the path.
117
      return readable( fqfn.toFile() )
118
      return readable( toFile( fqfn ) )
118119
        ? valid( link, url )
119120
        : resolveExtensionlessImageFile( link, node, url );
...
145146
        final var baseImagesDir = baseDir.resolve( imagesDir );
146147
        final var imagePath = baseImagesDir.resolve( url );
147
        final var file = resolveImageExtension( imagePath.toFile() );
148
        final var file = resolveImageExtension( toFile( imagePath ) );
148149
149150
        if( file.isPresent() ) {
M src/main/java/com/keenwrite/processors/r/RBootstrapController.java
6868
6969
    if( !bootstrap.isBlank() ) {
70
      final var map = new HashMap<String, String>( definitions.size() + 1 );
70
      final Map<String, String> map;
7171
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
7582
      map.put(
7683
        KEY_OPERATOR.apply( "application.r.working.directory" ),
M src/main/java/com/keenwrite/search/SearchModel.java
1010
import org.ahocorasick.trie.Trie;
1111
12
import java.util.ArrayList;
1312
import java.util.List;
1413
M src/main/java/com/keenwrite/typesetting/HostTypesetter.java
1717
import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY;
1818
import static com.keenwrite.events.StatusEvent.clue;
19
import static com.keenwrite.io.SysFile.toFile;
1920
import static java.lang.ProcessBuilder.Redirect.DISCARD;
2021
import static java.nio.file.Files.*;
...
108109
      final var stdout = new CircularQueue<String>( 150 );
109110
      final var builder = new ProcessBuilder( mArgs );
110
      builder.directory( mDirectory.toFile() );
111
      builder.directory( toFile( mDirectory ) );
111112
      builder.environment().put( "TEXMFCACHE", getCacheDir().toString() );
112113
...
203204
    private java.io.File getCacheDir() {
204205
      final var cache = Path.of( TEMPORARY_DIRECTORY, "luatex-cache" );
205
      return cache.toFile();
206
      return toFile( cache );
206207
    }
207208
M src/main/java/com/keenwrite/typesetting/containerization/Podman.java
1313
import static com.keenwrite.Bootstrap.CONTAINER_VERSION;
1414
import static com.keenwrite.events.StatusEvent.clue;
15
import static com.keenwrite.io.SysFile.toFile;
1516
import static java.lang.String.format;
1617
import static java.lang.String.join;
...
4950
  public static boolean canRun() {
5051
    try {
51
      return getExecutable().toFile().isFile();
52
      return toFile( getExecutable() ).isFile();
5253
    } catch( final Exception ex ) {
5354
      clue( "Wizard.container.executable.run.error", ex );
...
151152
    assert guestDir != null;
152153
    assert !guestDir.isBlank();
153
    assert hostDir.toFile().isDirectory();
154
    assert toFile( hostDir ).isDirectory();
154155
155156
    mMountPoints.add(
...
217218
  private static ProcessBuilder processBuilder(
218219
    final Path path, final String... s ) {
219
    return processBuilder( path.toFile(), s );
220
    return processBuilder( toFile( path ), s );
220221
  }
221222
M src/main/java/com/keenwrite/typesetting/installer/panes/AbstractDownloadPane.java
1616
import static com.keenwrite.Messages.getUri;
1717
import static com.keenwrite.events.StatusEvent.clue;
18
import static com.keenwrite.io.SysFile.toFile;
1819
1920
/**
...
3233
    mFilename = toFilename( mUri );
3334
    final var directory = USER_DATA_DIR;
34
    mTarget = directory.resolve( mFilename ).toFile();
35
    mTarget = toFile( directory.resolve( mFilename ) );
3536
    final var source = labelf( getPrefix() + ".paths", mFilename, directory );
3637
    mStatus = labelf( getPrefix() + STATUS + ".progress", 0, 0 );
M src/main/java/com/keenwrite/typesetting/installer/panes/InstallerPane.java
2727
import static com.keenwrite.Messages.get;
2828
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
29
import static com.keenwrite.io.SysFile.toFile;
2930
import static java.lang.System.lineSeparator;
3031
import static javafx.animation.Interpolator.LINEAR;
...
307308
308309
  static String toFilename( final URI uri ) {
309
    return Paths.get( uri.getPath() ).toFile().getName();
310
    return toFile( Paths.get( uri.getPath() ) ).getName();
310311
  }
311312
}
M src/main/java/com/keenwrite/typesetting/installer/panes/TypesetterThemesDownloadPane.java
1313
import static com.keenwrite.Messages.get;
1414
import static com.keenwrite.events.StatusEvent.clue;
15
import static com.keenwrite.io.SysFile.toFile;
1516
import static com.keenwrite.preferences.AppKeys.KEY_TYPESET_CONTEXT_THEMES_PATH;
1617
...
5657
5758
    // 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() ) );
5960
6061
    // Make sure the typesetter will know where to find the themes.
M src/main/java/com/keenwrite/ui/controls/BrowseFileButton.java
4444
import java.util.List;
4545
46
import static com.keenwrite.io.SysFile.toFile;
4647
import static com.keenwrite.ui.fonts.IconFactory.createGraphic;
4748
import static org.controlsfx.glyphfont.FontAwesome.Glyph.FILE_ALT;
...
8788
    fileChooser.getExtensionFilters()
8889
               .add( new ExtensionFilter( Messages.get(
89
                   "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
90
                 "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
9091
    fileChooser.setInitialDirectory( getInitialDirectory() );
9192
    var result = fileChooser.showOpenDialog( getScene().getWindow() );
9293
    if( result != null ) {
9394
      updateUrl( result );
9495
    }
9596
  }
9697
9798
  private File getInitialDirectory() {
9899
    //TODO build initial directory based on current value of 'url' property
99
    return getBasePath().toFile();
100
    return toFile( getBasePath() );
100101
  }
101102
M src/main/java/com/keenwrite/ui/dialogs/ExportDialog.java
3838
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
3939
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 ) );
359360
      final var in = new InputStreamReader( f, UTF_8 )
360361
    ) {
M src/main/java/com/keenwrite/ui/explorer/FilePickerFactory.java
1515
import java.util.Optional;
1616
17
import static com.keenwrite.io.SysFile.toFile;
1718
import static com.keenwrite.preferences.AppKeys.KEY_UI_RECENT_DIR;
1819
import static com.keenwrite.ui.explorer.FilePickerFactory.SelectionType.*;
...
9293
    @Override
9394
    public void setInitialDirectory( final Path path ) {
94
      assert path != null;
95
96
      final var file = path.toFile();
95
      final var file = toFile( path );
9796
9897
      mChooser.setInitialDirectory(
M src/main/java/com/keenwrite/ui/explorer/FilesView.java
2626
import static com.keenwrite.constants.Constants.UI_CONTROL_SPACING;
2727
import static com.keenwrite.events.StatusEvent.clue;
28
import static com.keenwrite.io.SysFile.toFile;
2829
import static com.keenwrite.ui.fonts.IconFactory.createFileIcon;
2930
import static java.nio.file.Files.size;
...
170171
          final var filename = entry.nameProperty().get();
171172
          final var path = Path.of( dir.toString(), filename );
172
          final var file = path.toFile();
173
          final var file = toFile( path );
173174
174175
          if( file.isFile() ) {
175176
            FileOpenEvent.fire( path.toUri() );
176177
          }
177178
          else if( file.isDirectory() ) {
178
            mDirectory.set( path.normalize().toFile() );
179
            mDirectory.set( toFile( path.normalize() ) );
179180
          }
180181
        }
...
271272
        SysFile.getFileName( path ),
272273
        size( path ),
273
        ofEpochMilli( path.toFile().lastModified() )
274
        ofEpochMilli( toFile( path ).lastModified() )
274275
      );
275276
    }
M src/main/java/com/keenwrite/ui/heuristics/DocumentStatistics.java
77
import com.keenwrite.ui.actions.Keyboard;
88
import com.keenwrite.ui.clipboard.Clipboard;
9
import com.whitemagicsoftware.wordcount.TokenizerException;
9
import com.whitemagicsoftware.keencount.TokenizerException;
1010
import javafx.beans.property.IntegerProperty;
1111
import javafx.beans.property.SimpleIntegerProperty;
M src/main/java/com/keenwrite/ui/heuristics/WordCounter.java
22
package com.keenwrite.ui.heuristics;
33
4
import com.whitemagicsoftware.wordcount.Tokenizer;
5
import com.whitemagicsoftware.wordcount.TokenizerFactory;
4
import com.whitemagicsoftware.keencount.Tokenizer;
5
import com.whitemagicsoftware.keencount.TokenizerFactory;
66
77
import java.util.Locale;
M src/main/java/com/keenwrite/ui/spelling/TextEditorSpellChecker.java
7878
    // paragraph, instead.
7979
    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();
8182
82
      paraId = Math.min( paraId + 1, paragraphs - 1 );
83
      paraId = Math.min( paraId + 1, count - 1 );
8384
      paragraph = editor.getParagraph( paraId );
8485
      text = paragraph.getText();
M src/test/java/com/keenwrite/io/UserDataDirTest.java
44
import org.junit.jupiter.api.Test;
55
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;
98
109
class UserDataDirTest {
1110
  @Test
12
  void test_Unix_GetAppDirectory_DirectoryExists()
13
    throws FileNotFoundException {
11
  void test_Unix_GetAppDirectory_DirectoryExists() {
1412
    final var path = UserDataDir.getAppPath( "test" );
1513
    final var file = path.toFile();