Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M build.gradle
6060
  //implementation 'org.controlsfx:controlsfx:11.1.0'
6161
  implementation 'org.fxmisc.richtext:richtextfx:0.10.9'
62
  implementation 'org.fxmisc.flowless:flowless:0.6.9'
62
  implementation 'org.fxmisc.flowless:flowless:0.6.10'
6363
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
6464
  implementation 'com.miglayout:miglayout-javafx:11.0'
M src/main/java/com/keenwrite/Launcher.java
1616
import static com.keenwrite.security.PermissiveCertificate.installTrustManager;
1717
import static java.lang.String.format;
18
import static picocli.CommandLine.*;
19
import static picocli.CommandLine.UnmatchedArgumentException.*;
1820
1921
/**
...
3133
   */
3234
  private final String[] mArgs;
35
36
  /**
37
   * Responsible for informing the user of an invalid command-line option,
38
   * along with suggestions as to the closest argument name that matches.
39
   */
40
  private static final class ArgHandler implements IParameterExceptionHandler {
41
    /**
42
     * Invoked by the command-line parser when an invalid option is provided.
43
     *
44
     * @param ex   Captures information about the parameter.
45
     * @param args Captures the complete command-line arguments.
46
     * @return The application exit code (non-zero).
47
     */
48
    public int handleParseException(
49
      final ParameterException ex, final String[] args ) {
50
      final var cmd = ex.getCommandLine();
51
      final var writer = cmd.getErr();
52
      final var spec = cmd.getCommandSpec();
53
      final var mapper = cmd.getExitCodeExceptionMapper();
54
55
      writer.println( ex.getMessage() );
56
      printSuggestions( ex, writer );
57
      writer.print( cmd.getHelp().fullSynopsis() );
58
      writer.printf( "Run '%s --help' for details.%n", spec.qualifiedName() );
59
60
      return mapper == null
61
        ? spec.exitCodeOnInvalidInput()
62
        : mapper.getExitCode( ex );
63
    }
64
  }
3365
3466
  /**
...
6597
6698
    parser.setColorScheme( ColourScheme.create() );
99
    parser.setParameterExceptionHandler( new ArgHandler() );
100
    parser.setUnmatchedArgumentsAllowed( false );
67101
68102
    final var exitCode = parser.execute( args );
M src/main/java/com/keenwrite/cmdline/Arguments.java
6666
  private boolean mDebug;
6767
68
6869
  @CommandLine.Option(
6970
    names = {"-i", "--input"},
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 java.io.File.createTempFile;
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
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 valueFrom( 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 valueFrom( final Path path ) {
204
    assert path != null;
205
    return valueFrom( 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
  public boolean isType( final TypeName typeName ) {
280
    return mTypeName == typeName;
281
  }
282
283
  /**
284
   * Answers whether this instance is a scalable vector graphic.
285
   *
286
   * @return {@code true} if this instance represents an SVG object.
287
   */
288
  public boolean isSvg() {
289
    return this == IMAGE_SVG_XML;
290
  }
291
292
  public boolean isUndefined() {
293
    return this == UNDEFINED;
294
  }
295
296
  /**
297
   * Returns the IANA-defined subtype classification. Primarily used by
298
   * {@link MediaTypeExtension} to initialize associations where the subtype
299
   * name and the file name extension have a 1:1 mapping.
300
   *
301
   * @return The IANA subtype value.
302
   */
303
  public String getSubtype() {
304
    return mSubtype;
305
  }
306
307
  /**
308
   * Creates a temporary {@link File} that starts with the given prefix. The
309
   * file will be deleted when the application exits.
310
   *
311
   * @param prefix The file name begins with this string (may be empty).
312
   * @return The fully qualified path to the temporary file.
313
   * @throws IOException Could not create the temporary file.
314
   */
315
  public Path createTemporaryFile( final String prefix ) throws IOException {
316
    assert prefix != null;
317
318
    final var file = createTempFile(
319
      prefix, '.' + MediaTypeExtension.valueFrom( this ).getExtension() );
320
    file.deleteOnExit();
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
public enum MediaType {
21
  APP_DOCUMENT_OUTLINE( APPLICATION, "x-document-outline" ),
22
  APP_DOCUMENT_STATISTICS( APPLICATION, "x-document-statistics" ),
23
  APP_FILE_MANAGER( APPLICATION, "x-file-manager" ),
24
25
  APP_ACAD( APPLICATION, "acad" ),
26
  APP_JAVA_OBJECT( APPLICATION, "x-java-serialized-object" ),
27
  APP_JAVA( APPLICATION, "java" ),
28
  APP_PS( APPLICATION, "postscript" ),
29
  APP_EPS( APPLICATION, "eps" ),
30
  APP_PDF( APPLICATION, "pdf" ),
31
  APP_ZIP( APPLICATION, "zip" ),
32
33
  /*
34
   * Standard font types.
35
   */
36
  FONT_OTF( "otf" ),
37
  FONT_TTF( "ttf" ),
38
39
  /*
40
   * Standard image types.
41
   */
42
  IMAGE_APNG( "apng" ),
43
  IMAGE_ACES( "aces" ),
44
  IMAGE_AVCI( "avci" ),
45
  IMAGE_AVCS( "avcs" ),
46
  IMAGE_BMP( "bmp" ),
47
  IMAGE_CGM( "cgm" ),
48
  IMAGE_DICOM_RLE( "dicom_rle" ),
49
  IMAGE_EMF( "emf" ),
50
  IMAGE_EXAMPLE( "example" ),
51
  IMAGE_FITS( "fits" ),
52
  IMAGE_G3FAX( "g3fax" ),
53
  IMAGE_GIF( "gif" ),
54
  IMAGE_HEIC( "heic" ),
55
  IMAGE_HEIF( "heif" ),
56
  IMAGE_HEJ2K( "hej2k" ),
57
  IMAGE_HSJ2( "hsj2" ),
58
  IMAGE_X_ICON( "x-icon" ),
59
  IMAGE_JLS( "jls" ),
60
  IMAGE_JP2( "jp2" ),
61
  IMAGE_JPEG( "jpeg" ),
62
  IMAGE_JPH( "jph" ),
63
  IMAGE_JPHC( "jphc" ),
64
  IMAGE_JPM( "jpm" ),
65
  IMAGE_JPX( "jpx" ),
66
  IMAGE_JXR( "jxr" ),
67
  IMAGE_JXRA( "jxrA" ),
68
  IMAGE_JXRS( "jxrS" ),
69
  IMAGE_JXS( "jxs" ),
70
  IMAGE_JXSC( "jxsc" ),
71
  IMAGE_JXSI( "jxsi" ),
72
  IMAGE_JXSS( "jxss" ),
73
  IMAGE_KTX( "ktx" ),
74
  IMAGE_KTX2( "ktx2" ),
75
  IMAGE_NAPLPS( "naplps" ),
76
  IMAGE_PNG( "png" ),
77
  IMAGE_PHOTOSHOP( "photoshop" ),
78
  IMAGE_SVG_XML( "svg+xml" ),
79
  IMAGE_T38( "t38" ),
80
  IMAGE_TIFF( "tiff" ),
81
  IMAGE_WEBP( "webp" ),
82
  IMAGE_WMF( "wmf" ),
83
  IMAGE_X_BITMAP( "x-xbitmap" ),
84
  IMAGE_X_PIXMAP( "x-xpixmap" ),
85
86
  /*
87
   * Standard audio types.
88
   */
89
  AUDIO_SIMPLE( AUDIO, "basic" ),
90
  AUDIO_MP3( AUDIO, "mp3" ),
91
  AUDIO_WAV( AUDIO, "x-wav" ),
92
93
  /*
94
   * Standard video types.
95
   */
96
  VIDEO_MNG( VIDEO, "x-mng" ),
97
98
  /*
99
   * Document types for editing or displaying documents, mix of standard and
100
   * application-specific. The order that these are declared reflect in the
101
   * ordinal value used during comparisons.
102
   */
103
  TEXT_YAML( TEXT, "yaml" ),
104
  TEXT_PLAIN( TEXT, "plain" ),
105
  TEXT_MARKDOWN( TEXT, "markdown" ),
106
  TEXT_R_MARKDOWN( TEXT, "R+markdown" ),
107
  TEXT_PROPERTIES( TEXT, "x-java-properties" ),
108
  TEXT_HTML( TEXT, "html" ),
109
  TEXT_XHTML( TEXT, "xhtml+xml" ),
110
  TEXT_XML( TEXT, "xml" ),
111
112
  /*
113
   * When all other lights go out.
114
   */
115
  UNDEFINED( TypeName.UNDEFINED, "undefined" );
116
117
  /**
118
   * The IANA-defined types.
119
   */
120
  public enum TypeName {
121
    APPLICATION,
122
    AUDIO,
123
    IMAGE,
124
    TEXT,
125
    UNDEFINED,
126
    VIDEO
127
  }
128
129
  /**
130
   * The fully qualified IANA-defined media type.
131
   */
132
  private final String mMediaType;
133
134
  /**
135
   * The IANA-defined type name.
136
   */
137
  private final TypeName mTypeName;
138
139
  /**
140
   * The IANA-defined subtype name.
141
   */
142
  private final String mSubtype;
143
144
  /**
145
   * Constructs an instance using the default type name of "image".
146
   *
147
   * @param subtype The image subtype name.
148
   */
149
  MediaType( final String subtype ) {
150
    this( IMAGE, subtype );
151
  }
152
153
  /**
154
   * Constructs an instance using an IANA-defined type and subtype pair.
155
   *
156
   * @param typeName The media type's type name.
157
   * @param subtype  The media type's subtype name.
158
   */
159
  MediaType( final TypeName typeName, final String subtype ) {
160
    mTypeName = typeName;
161
    mSubtype = subtype;
162
    mMediaType = typeName.toString().toLowerCase() + '/' + subtype;
163
  }
164
165
  /**
166
   * Returns the {@link MediaType} associated with the given file.
167
   *
168
   * @param file Has a file name that may contain an extension associated with
169
   *             a known {@link MediaType}.
170
   * @return {@link MediaType#UNDEFINED} if the extension has not been
171
   * assigned, otherwise the {@link MediaType} associated with this
172
   * {@link File}'s file name extension.
173
   */
174
  public static MediaType valueFrom( final File file ) {
175
    assert file != null;
176
    return fromFilename( file.getName() );
177
  }
178
179
  /**
180
   * Returns the {@link MediaType} associated with the given file name.
181
   *
182
   * @param filename The file name that may contain an extension associated
183
   *                 with a known {@link MediaType}.
184
   * @return {@link MediaType#UNDEFINED} if the extension has not been
185
   * assigned, otherwise the {@link MediaType} associated with this
186
   * {@link URL}'s file name extension.
187
   */
188
  public static MediaType fromFilename( final String filename ) {
189
    assert filename != null;
190
    return fromExtension( getExtension( filename ) );
191
  }
192
193
  /**
194
   * Returns the {@link MediaType} associated with the path to a file.
195
   *
196
   * @param path Has a file name that may contain an extension associated with
197
   *             a known {@link MediaType}.
198
   * @return {@link MediaType#UNDEFINED} if the extension has not been
199
   * assigned, otherwise the {@link MediaType} associated with this
200
   * {@link File}'s file name extension.
201
   */
202
  public static MediaType valueFrom( final Path path ) {
203
    assert path != null;
204
    return valueFrom( path.toFile() );
205
  }
206
207
  /**
208
   * Determines the media type an IANA-defined, semi-colon-separated string.
209
   * This is often used after making an HTTP request to extract the type
210
   * and subtype from the content-type.
211
   *
212
   * @param header The content-type header value, may be {@code null}.
213
   * @return The data type for the resource or {@link MediaType#UNDEFINED} if
214
   * unmapped.
215
   */
216
  public static MediaType valueFrom( String header ) {
217
    if( header == null || header.isBlank() ) {
218
      return UNDEFINED;
219
    }
220
221
    // Trim off the character encoding.
222
    var i = header.indexOf( ';' );
223
    header = header.substring( 0, i == -1 ? header.length() : i );
224
225
    // Split the type and subtype.
226
    i = header.indexOf( '/' );
227
    i = i == -1 ? header.length() : i;
228
    final var type = header.substring( 0, i );
229
    final var subtype = header.substring( i + 1 );
230
231
    return valueFrom( type, subtype );
232
  }
233
234
  /**
235
   * Returns the {@link MediaType} for the given type and subtype names.
236
   *
237
   * @param type    The IANA-defined type name.
238
   * @param subtype The IANA-defined subtype name.
239
   * @return {@link MediaType#UNDEFINED} if there is no {@link MediaType} that
240
   * matches the given type and subtype names.
241
   */
242
  public static MediaType valueFrom(
243
    final String type, final String subtype ) {
244
    assert type != null;
245
    assert subtype != null;
246
247
    for( final var mediaType : values() ) {
248
      if( mediaType.equals( type, subtype ) ) {
249
        return mediaType;
250
      }
251
    }
252
253
    return UNDEFINED;
254
  }
255
256
  /**
257
   * Answers whether the given type and subtype names equal this enumerated
258
   * value. This performs a case-insensitive comparison.
259
   *
260
   * @param type    The type name to compare against this {@link MediaType}.
261
   * @param subtype The subtype name to compare against this {@link MediaType}.
262
   * @return {@code true} when the type and subtype name match.
263
   */
264
  public boolean equals( final String type, final String subtype ) {
265
    assert type != null;
266
    assert subtype != null;
267
268
    return mTypeName.name().equalsIgnoreCase( type ) &&
269
      mSubtype.equalsIgnoreCase( subtype );
270
  }
271
272
  /**
273
   * Answers whether the given {@link TypeName} matches this type name.
274
   *
275
   * @param typeName The {@link TypeName} to compare against the internal value.
276
   * @return {@code true} if the given value is the same IANA-defined type name.
277
   */
278
  public boolean isType( final TypeName typeName ) {
279
    return mTypeName == typeName;
280
  }
281
282
  /**
283
   * Answers whether this instance is a scalable vector graphic.
284
   *
285
   * @return {@code true} if this instance represents an SVG object.
286
   */
287
  public boolean isSvg() {
288
    return this == IMAGE_SVG_XML;
289
  }
290
291
  public boolean isUndefined() {
292
    return this == UNDEFINED;
293
  }
294
295
  /**
296
   * Returns the IANA-defined subtype classification. Primarily used by
297
   * {@link MediaTypeExtension} to initialize associations where the subtype
298
   * name and the file name extension have a 1:1 mapping.
299
   *
300
   * @return The IANA subtype value.
301
   */
302
  public String getSubtype() {
303
    return mSubtype;
304
  }
305
306
  /**
307
   * Creates a temporary {@link File} that starts with the given prefix.
308
   *
309
   * @param prefix The file name begins with this string (may be empty).
310
   * @return The fully qualified path to the temporary file.
311
   * @throws IOException Could not create the temporary file.
312
   */
313
  public Path createTempFile( final String prefix ) throws IOException {
314
    return createTempFile( prefix, false );
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 (may be empty).
321
   * @param purge  Set to {@code true} to delete the file on exit.
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, final boolean purge ) throws IOException {
327
    assert prefix != null;
328
329
    final var file = File.createTempFile(
330
      prefix, '.' + MediaTypeExtension.valueFrom( this ).getExtension() );
331
332
    if( purge ) {
333
      file.deleteOnExit();
334
    }
335
321336
    return file.toPath();
322337
  }
M src/main/java/com/keenwrite/processors/PdfProcessor.java
2626
2727
  /**
28
   * Converts a document by calling a third-party library to typeset the given
29
   * XHTML document.
28
   * Converts a document by calling a third-party application to typeset the
29
   * given XHTML document.
3030
   *
3131
   * @param xhtml The document to convert to a PDF file.
3232
   * @return {@code null} because there is no valid return value from generating
3333
   * a PDF file.
3434
   */
3535
  public String apply( final String xhtml ) {
3636
    try {
3737
      clue( "Main.status.typeset.create" );
3838
      final var context = mContext;
39
      final var document = TEXT_XML.createTemporaryFile( APP_TITLE_LOWERCASE );
39
      final var document = TEXT_XML.createTempFile( APP_TITLE_LOWERCASE );
4040
      final var typesetter = Typesetter
4141
        .builder()
...
5151
      if( typesetter.autoclean() ) {
5252
        deleteIfExists( document );
53
      }
54
      else {
55
        document.toFile().deleteOnExit();
5653
      }
5754
    } catch( final IOException | InterruptedException ex ) {
M src/main/java/com/keenwrite/processors/ProcessorContext.java
164164
    public void setImageDir( final Supplier<File> imageDir ) {
165165
      assert imageDir != null;
166
      mImageDir = () -> imageDir.get().toPath();
166
167
      mImageDir = () -> {
168
        final var dir = imageDir.get();
169
170
        return (dir == null ? USER_DIRECTORY : dir).toPath();
171
      };
167172
    }
168173
M src/main/java/com/keenwrite/processors/XhtmlProcessor.java
7979
        try {
8080
          final var attrs = node.getAttributes();
81
8281
          final var attr = attrs.getNamedItem( "src" );
8382
...
110109
  private void setMetaData( final Document doc ) {
111110
    final var metadata = createMetaDataMap( doc );
111
112112
    visit( doc, "/html/head", node ->
113113
      metadata.entrySet()
114114
              .forEach( entry -> node.appendChild( createMeta( doc, entry ) ) )
115115
    );
116116
117117
    final var title = metadata.get( "title" );
118
118119
    if( title != null ) {
119120
      visit( doc, "/html/head/title", node -> node.setTextContent( title ) );
...
161162
   */
162163
  private Path exportImage( final String src ) throws Exception {
163
    Path imageFile = null;
164
    return getProtocol( src ).isRemote()
165
      ? downloadImage( src )
166
      : resolveImage( src );
167
  }
164168
165
    final var protocol = getProtocol( src );
169
  private Path downloadImage( final String src ) throws Exception {
170
    final Path imageFile;
166171
167
    // Download remote resources into temporary files.
168
    if( protocol.isRemote() ) {
169
      try( final var response = httpGet( src ) ) {
170
        final var mediaType = response.getMediaType();
172
    clue( "Main.status.image.xhtml.image.download", src );
171173
172
        imageFile = mediaType.createTemporaryFile( APP_TITLE_LOWERCASE );
174
    try( final var response = httpGet( src ) ) {
175
      final var mediaType = response.getMediaType();
173176
174
        try( final var image = response.getInputStream() ) {
175
          copy( image, imageFile, REPLACE_EXISTING );
176
        }
177
      // Preserve image files if autoclean is turned off.
178
      imageFile = mediaType.createTempFile( APP_TITLE_LOWERCASE, autoclean() );
177179
178
        // Strip comments, superfluous whitespace, DOCTYPE, and XML
179
        // declarations.
180
        if( mediaType.isSvg() ) {
181
          DocumentParser.sanitize( imageFile );
182
        }
180
      try( final var image = response.getInputStream() ) {
181
        copy( image, imageFile, REPLACE_EXISTING );
182
      }
183
184
      // Strip comments, superfluous whitespace, DOCTYPE, and XML
185
      // declarations.
186
      if( mediaType.isSvg() ) {
187
        DocumentParser.sanitize( imageFile );
183188
      }
184189
    }
185
    else {
186
      final var extensions = getImageOrder();
187
      var imagePath = getImagePath();
188
      var found = false;
189190
190
      for( final var extension : extensions ) {
191
        final var filename = format(
192
          "%s%s%s", src, extension.isBlank() ? "" : ".", extension );
193
        imageFile = Path.of( imagePath, filename );
191
    return imageFile;
192
  }
194193
195
        if( imageFile.toFile().exists() ) {
196
          found = true;
197
          break;
198
        }
194
  private Path resolveImage( final String src ) throws Exception {
195
    var imagePath = getImagePath();
196
    var found = false;
197
198
    Path imageFile = null;
199
200
    clue( "Main.status.image.xhtml.image.resolve", src );
201
202
    for( final var extension : getImageOrder() ) {
203
      final var filename = format(
204
        "%s%s%s", src, extension.isBlank() ? "" : ".", extension );
205
      imageFile = Path.of( imagePath, filename );
206
207
      if( imageFile.toFile().exists() ) {
208
        found = true;
209
        break;
199210
      }
211
    }
200212
201
      if( !found ) {
202
        imagePath = getDocumentDir().toString();
203
        imageFile = Path.of( imagePath, src );
213
    if( !found ) {
214
      imagePath = getDocumentDir().toString();
215
      imageFile = Path.of( imagePath, src );
204216
205
        if( !imageFile.toFile().exists() ) {
206
          throw new FileNotFoundException( imageFile.toString() );
207
        }
217
      if( !imageFile.toFile().exists() ) {
218
        final var filename = imageFile.toString();
219
        clue( "Main.status.image.xhtml.image.missing", filename );
220
221
        throw new FileNotFoundException( filename );
208222
      }
209223
    }
224
225
    clue( "Main.status.image.xhtml.image.found", imageFile.toString() );
210226
211227
    return imageFile;
...
240256
  private Locale getLocale() {
241257
    return mContext.getLocale();
258
  }
259
260
  private boolean autoclean() {
261
    return mContext.getAutoClean();
242262
  }
243263
M src/main/resources/com/keenwrite/messages.properties
171171
Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
172172
173
Main.status.image.xhtml.image.download=Downloading ''{0}''
174
Main.status.image.xhtml.image.resolve=Qualify path for ''{0}''
175
Main.status.image.xhtml.image.found=Found image ''{0}''
176
Main.status.image.xhtml.image.missing=Missing image ''{0}''
177
173178
Main.status.font.search.missing=No font name starting with ''{0}'' was found
174179
M src/main/resources/com/keenwrite/preview/webview.css
304304
}
305305
306
/* LYRICS ***/
307
div.lyrics {
308
  margin: 0;
309
  padding: 0;
310
  white-space: pre-line;
311
  font-style: italic;
312
}
313
314
div.lyrics:first-line {
315
  line-height: 0;
316
}
317
318