Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M build.gradle
1818
  id 'org.openjfx.javafxplugin' version '0.1.0'
1919
  id 'com.palantir.git-version' version '3.0.0'
20
  id 'com.github.spotbugs' version '5.2.4'
20
  id 'com.github.spotbugs' version '6.0.6'
2121
}
2222
...
9191
  def v_junit = '5.10.1'
9292
  def v_flexmark = '0.64.8'
93
  def v_jackson = '2.16.0'
93
  def v_jackson = '2.16.1'
9494
  def v_echosvg = '1.0.1'
9595
  def v_picocli = '4.7.5'
9696
9797
  // JavaFX
9898
  implementation 'org.controlsfx:controlsfx:11.2.0'
9999
  implementation 'org.fxmisc.richtext:richtextfx:0.11.2'
100100
  implementation 'org.fxmisc.flowless:flowless:0.7.2'
101101
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
102
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.16.0'
102
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.17.0'
103103
  implementation 'com.panemu:tiwulfx-dock:0.2'
104104
...
166166
  testImplementation "org.junit.jupiter:junit-jupiter-params:${v_junit}"
167167
  testImplementation 'org.testfx:testfx-junit5:4.0.17'
168
  testImplementation 'org.assertj:assertj-core:3.24.2'
168
  testImplementation 'org.assertj:assertj-core:3.25.1'
169169
  testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
170170
}
M installer.sh
3939
ARG_JAVA_OS="linux"
4040
ARG_JAVA_ARCH="amd64"
41
ARG_JAVA_VERSION="21.0.1"
42
ARG_JAVA_UPDATE="12"
41
ARG_JAVA_VERSION="21.0.2"
42
ARG_JAVA_UPDATE="14"
4343
ARG_JAVA_DIR="java"
4444
M keenwrite.sh
11
#!/usr/bin/env bash
22
3
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
4
35
java \
46
  -Dprism.order=sw \
...
1719
  --add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED \
1820
  --add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED \
19
  -jar keenwrite.jar $@
21
  -jar ${SCRIPT_DIR}/keenwrite.jar $@
2022
2123
M src/main/java/com/keenwrite/MainApp.java
2323
import static com.keenwrite.preferences.AppKeys.*;
2424
import static com.keenwrite.util.FontLoader.initFonts;
25
import static javafx.scene.input.KeyCode.ESCAPE;
2625
import static javafx.scene.input.KeyCode.F11;
2726
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
...
3736
3837
  private Workspace mWorkspace;
39
  private MainScene mMainScene;
4038
4139
  /**
...
115113
    // instance will be loaded and applied.
116114
    final var property = mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
117
    property.addListener( ( c, o, n ) -> readLexicon() );
115
    property.addListener( ( _, _, _ ) -> readLexicon() );
118116
119117
    initFonts();
...
126124
127125
    // Load the lexicon and check all the documents after all files are open.
128
    stage.addEventFilter( WINDOW_SHOWN, event -> readLexicon() );
126
    stage.addEventFilter( WINDOW_SHOWN, _ -> readLexicon() );
129127
    stage.show();
130128
...
157155
      if( F11.equals( event.getCode() ) ) {
158156
        stage.setFullScreen( !stage.isFullScreen() );
159
      }
160
    } );
161
162
    // After the app loses focus, when the user switches back using Alt+Tab,
163
    // the menu is engaged on Windows. Simulate an ESC keypress to the menu
164
    // to disable the menu, giving focus back to the application proper.
165
    //
166
    // JavaFX Bug: https://bugs.openjdk.java.net/browse/JDK-8090647
167
    stage.focusedProperty().addListener( ( c, lost, found ) -> {
168
      if( found ) {
169
        mMainScene.getMenuBar().fireEvent( keyDown( ESCAPE ) );
170157
      }
171158
    } );
172159
  }
173160
174161
  private void initIcons( final Stage stage ) {
175162
    stage.getIcons().addAll( LOGOS );
176163
  }
177164
178165
  private void initScene( final Stage stage ) {
179
    mMainScene = new MainScene( mWorkspace );
180
    stage.setScene( mMainScene.getScene() );
166
    final var mainScene = new MainScene( mWorkspace );
167
    stage.setScene( mainScene.getScene() );
181168
  }
182169
M src/main/java/com/keenwrite/MainPane.java
11261126
      .with( Mutator::setFontDir,
11271127
             () -> w.getFile( KEY_TYPESET_CONTEXT_FONTS_DIR ) )
1128
      .with( Mutator::setEnableMode,
1128
      .with( Mutator::setModesEnabled,
11291129
             () -> w.getString( KEY_TYPESET_MODES_ENABLED ) )
11301130
      .with( Mutator::setCurlQuotes,
M src/main/java/com/keenwrite/cmdline/Arguments.java
103103
    paramLabel = "String"
104104
  )
105
  private String mEnableMode;
105
  private String mTypesetMode;
106106
107107
  @CommandLine.Option(
...
264264
      .with( Mutator::setImageOrder, () -> mImageOrder )
265265
      .with( Mutator::setFontDir, () -> mFontDir )
266
      .with( Mutator::setEnableMode, () -> mEnableMode )
266
      .with( Mutator::setModesEnabled, () -> mTypesetMode )
267267
      .with( Mutator::setExportFormat, format )
268268
      .with( Mutator::setDefinitions, () -> definitions )
M src/main/java/com/keenwrite/collections/InterpolatingMap.java
9494
9595
      if( mapValue != null ) {
96
        if( mapValue.contains( mOperator.apply( keyName ) ) ) {
97
          throw new IllegalStateException(
98
            STR."Mapped value '\{mapValue}' may not contain its key name '\{keyName}'" );
99
        }
100
96101
        final var keyValue = interpolate( mapValue );
97102
        matcher.appendReplacement( sb, quoteReplacement( keyValue ) );
M src/main/java/com/keenwrite/events/StatusEvent.java
188188
  }
189189
190
  private static void fire( final String message ) {
190
  public static void fire( final String message ) {
191191
    new StatusEvent( message ).publish();
192192
  }
M src/main/java/com/keenwrite/processors/PdfProcessor.java
6666
      clue( "Main.status.typeset.setting", "r-work", rWorkDir );
6767
68
      final var enableMode = sanitize( context.getEnableMode() );
69
      clue( "Main.status.typeset.setting", "mode", enableMode );
68
      final var modesEnabled = sanitize( context.getModesEnabled() );
69
      clue( "Main.status.typeset.setting", "mode", modesEnabled );
7070
7171
      final var autoRemove = context.getAutoRemove();
...
8080
        .with( Mutator::setCacheDir, cacheDir )
8181
        .with( Mutator::setFontDir, fontDir )
82
        .with( Mutator::setEnableMode, enableMode )
82
        .with( Mutator::setModesEnabled, modesEnabled )
8383
        .with( Mutator::setAutoRemove, autoRemove )
8484
        .build();
M src/main/java/com/keenwrite/processors/ProcessorContext.java
3434
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
3535
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
36
37
/**
38
 * Provides a context for configuring a chain of {@link Processor} instances.
39
 */
40
public final class ProcessorContext {
41
42
  private final Mutator mMutator;
43
44
  /**
45
   * Determines the file type from the path extension. This should only be
46
   * called when it is known that the file type won't be a definition file
47
   * (e.g., YAML or other definition source), but rather an editable file
48
   * (e.g., Markdown, R Markdown, etc.).
49
   *
50
   * @param path The path with a file name extension.
51
   * @return The FileType for the given path.
52
   */
53
  private static FileType lookup( final Path path ) {
54
    assert path != null;
55
56
    final var prefix = GLOB_PREFIX_FILE;
57
    final var keys = sSettings.getKeys( prefix );
58
59
    var found = false;
60
    var fileType = UNKNOWN;
61
62
    while( keys.hasNext() && !found ) {
63
      final var key = keys.next();
64
      final var patterns = sSettings.getStringSettingList( key );
65
      final var predicate = createFileTypePredicate( patterns );
66
67
      if( predicate.test( toFile( path ) ) ) {
68
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
69
        // to a standard name (as defined in the settings.properties file).
70
        final String suffix = key.replace( prefix + '.', "" );
71
        fileType = FileType.from( suffix );
72
        found = true;
73
      }
74
    }
75
76
    return fileType;
77
  }
78
79
  public boolean isExportFormat( final ExportFormat exportFormat ) {
80
    return mMutator.mExportFormat == exportFormat;
81
  }
82
83
  /**
84
   * Responsible for populating the instance variables required by the
85
   * context.
86
   */
87
  public static class Mutator {
88
    private Path mSourcePath;
89
    private Path mTargetPath;
90
    private ExportFormat mExportFormat;
91
    private Supplier<Boolean> mConcatenate = () -> true;
92
    private Supplier<String> mChapters = () -> "";
93
94
    private Supplier<Path> mThemeDir = USER_DIRECTORY::toPath;
95
    private Supplier<Locale> mLocale = () -> Locale.ENGLISH;
96
97
    private Supplier<Map<String, String>> mDefinitions = HashMap::new;
98
    private Supplier<Map<String, String>> mMetadata = HashMap::new;
99
    private Supplier<Caret> mCaret = () -> Caret.builder().build();
100
101
    private Supplier<Path> mImageDir = USER_DIRECTORY::toPath;
102
    private Supplier<String> mImageServer = () -> DIAGRAM_SERVER_NAME;
103
    private Supplier<String> mImageOrder = () -> PERSIST_IMAGES_DEFAULT;
104
    private Supplier<Path> mCacheDir = USER_CACHE_DIR::toPath;
105
    private Supplier<Path> mFontDir = () -> getFontDirectory().toPath();
106
107
    private Supplier<String> mEnableMode = () -> "";
108
109
    private Supplier<String> mSigilBegan = () -> DEF_DELIM_BEGAN_DEFAULT;
110
    private Supplier<String> mSigilEnded = () -> DEF_DELIM_ENDED_DEFAULT;
111
112
    private Supplier<Path> mRWorkingDir = USER_DIRECTORY::toPath;
113
    private Supplier<String> mRScript = () -> "";
114
115
    private Supplier<Boolean> mCurlQuotes = () -> true;
116
    private Supplier<Boolean> mAutoRemove = () -> true;
117
118
    public void setSourcePath( final Path sourcePath ) {
119
      assert sourcePath != null;
120
      mSourcePath = sourcePath;
121
    }
122
123
    public void setTargetPath( final Path outputPath ) {
124
      assert outputPath != null;
125
      mTargetPath = outputPath;
126
    }
127
128
    public void setThemeDir( final Supplier<Path> themeDir ) {
129
      assert themeDir != null;
130
      mThemeDir = themeDir;
131
    }
132
133
    public void setCacheDir( final Supplier<File> cacheDir ) {
134
      assert cacheDir != null;
135
136
      mCacheDir = () -> {
137
        final var dir = cacheDir.get();
138
139
        return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
140
      };
141
    }
142
143
    public void setImageDir( final Supplier<File> imageDir ) {
144
      assert imageDir != null;
145
146
      mImageDir = () -> {
147
        final var dir = imageDir.get();
148
149
        return (dir == null ? USER_DIRECTORY : dir).toPath();
150
      };
151
    }
152
153
    public void setImageOrder( final Supplier<String> imageOrder ) {
154
      assert imageOrder != null;
155
      mImageOrder = imageOrder;
156
    }
157
158
    public void setImageServer( final Supplier<String> imageServer ) {
159
      assert imageServer != null;
160
      mImageServer = imageServer;
161
    }
162
163
    public void setFontDir( final Supplier<File> fontDir ) {
164
      assert fontDir != null;
165
166
      mFontDir = () -> {
167
        final var dir = fontDir.get();
168
169
        return (dir == null ? USER_DIRECTORY : dir).toPath();
170
      };
171
    }
172
173
    public void setEnableMode( final Supplier<String> enableMode ) {
174
      assert enableMode != null;
175
      mEnableMode = enableMode;
176
    }
177
178
    public void setExportFormat( final ExportFormat exportFormat ) {
179
      assert exportFormat != null;
180
      mExportFormat = exportFormat;
181
    }
182
183
    public void setConcatenate( final Supplier<Boolean> concatenate ) {
184
      mConcatenate = concatenate;
185
    }
186
187
    public void setChapters( final Supplier<String> chapters ) {
188
      mChapters = chapters;
189
    }
190
191
    public void setLocale( final Supplier<Locale> locale ) {
192
      assert locale != null;
193
      mLocale = locale;
194
    }
195
196
    /**
197
     * Sets the list of fully interpolated key-value pairs to use when
198
     * substituting variable names back into the document as variable values.
199
     * This uses a {@link Callable} reference so that GUI and command-line
200
     * usage can insert their respective behaviours. That is, this method
201
     * prevents coupling the GUI to the CLI.
202
     *
203
     * @param supplier Defines how to retrieve the definitions.
204
     */
205
    public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
206
      assert supplier != null;
207
      mDefinitions = supplier;
208
    }
209
210
    /**
211
     * Sets metadata to use in the document header. These are made available
212
     * to the typesetting engine as {@code \documentvariable} values.
213
     *
214
     * @param metadata The key/value pairs to publish as document metadata.
215
     */
216
    public void setMetadata( final Supplier<Map<String, String>> metadata ) {
217
      assert metadata != null;
218
      mMetadata = metadata.get() == null ? HashMap::new : metadata;
219
    }
220
221
    /**
222
     * Sets document variables to use when building the document. These
223
     * variables will override existing key/value pairs, or be added as
224
     * new key/value pairs if not already defined. This allows users to
225
     * inject variables into the document from the command-line, allowing
226
     * for dynamic assignment of in-text values when building documents.
227
     *
228
     * @param overrides The key/value pairs to add (or override) as variables.
229
     */
230
    public void setOverrides( final Supplier<Map<String, String>> overrides ) {
231
      assert overrides != null;
232
      assert mDefinitions != null;
233
      assert mDefinitions.get() != null;
234
235
      final var map = overrides.get();
236
237
      if( map != null ) {
238
        mDefinitions.get().putAll( map );
239
      }
240
    }
241
242
    /**
243
     * Sets the source for deriving the {@link Caret}. Typically, this is
244
     * the text editor that has focus.
245
     *
246
     * @param caret The source for the currently active caret.
247
     */
248
    public void setCaret( final Supplier<Caret> caret ) {
249
      assert caret != null;
250
      mCaret = caret;
251
    }
252
253
    public void setSigilBegan( final Supplier<String> sigilBegan ) {
254
      assert sigilBegan != null;
255
      mSigilBegan = sigilBegan;
256
    }
257
258
    public void setSigilEnded( final Supplier<String> sigilEnded ) {
259
      assert sigilEnded != null;
260
      mSigilEnded = sigilEnded;
261
    }
262
263
    public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
264
      assert rWorkingDir != null;
265
      mRWorkingDir = rWorkingDir;
266
    }
267
268
    public void setRScript( final Supplier<String> rScript ) {
269
      assert rScript != null;
270
      mRScript = rScript;
271
    }
272
273
    public void setCurlQuotes( final Supplier<Boolean> curlQuotes ) {
274
      assert curlQuotes != null;
275
      mCurlQuotes = curlQuotes;
276
    }
277
278
    public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
279
      assert autoRemove != null;
280
      mAutoRemove = autoRemove;
281
    }
282
283
    private boolean isExportFormat( final ExportFormat format ) {
284
      return mExportFormat == format;
285
    }
286
  }
287
288
  public static GenericBuilder<Mutator, ProcessorContext> builder() {
289
    return GenericBuilder.of( Mutator::new, ProcessorContext::new );
290
  }
291
292
  /**
293
   * Creates a new context for use by the {@link ProcessorFactory} when
294
   * instantiating new {@link Processor} instances. Although all the
295
   * parameters are required, not all {@link Processor} instances will use
296
   * all parameters.
297
   */
298
  private ProcessorContext( final Mutator mutator ) {
299
    assert mutator != null;
300
301
    mMutator = mutator;
302
  }
303
304
  public Path getSourcePath() {
305
    return mMutator.mSourcePath;
306
  }
307
308
  /**
309
   * Answers what type of input document is to be processed.
310
   *
311
   * @return The input document's {@link MediaType}.
312
   */
313
  public MediaType getSourceType() {
314
    return MediaTypeExtension.fromPath( mMutator.mSourcePath );
315
  }
316
317
  /**
318
   * Fully qualified file name to use when exporting (e.g., document.pdf).
319
   *
320
   * @return Full path to a file name.
321
   */
322
  public Path getTargetPath() {
323
    return mMutator.mTargetPath;
324
  }
325
326
  public ExportFormat getExportFormat() {
327
    return mMutator.mExportFormat;
328
  }
329
330
  public Locale getLocale() {
331
    return mMutator.mLocale.get();
332
  }
333
334
  /**
335
   * Returns the variable map of definitions, without interpolation.
336
   *
337
   * @return A map to help dereference variables.
338
   */
339
  public Map<String, String> getDefinitions() {
340
    return mMutator.mDefinitions.get();
341
  }
342
343
  /**
344
   * Returns the variable map of definitions, with interpolation.
345
   *
346
   * @return A map to help dereference variables.
347
   */
348
  public InterpolatingMap getInterpolatedDefinitions() {
349
    return new InterpolatingMap(
350
      createDefinitionKeyOperator(), getDefinitions()
351
    ).interpolate();
352
  }
353
354
  public Map<String, String> getMetadata() {
355
    return mMutator.mMetadata.get();
356
  }
357
358
  /**
359
   * Returns the current caret position in the document being edited and is
360
   * always up-to-date.
361
   *
362
   * @return Caret position in the document.
363
   */
364
  public Supplier<Caret> getCaret() {
365
    return mMutator.mCaret;
366
  }
367
368
  /**
369
   * Returns the directory that contains the file being edited. When
370
   * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
371
   * {@code null}. This will get absolute path to the file before trying to
372
   * get te parent path, which should always be a valid path. In the unlikely
373
   * event that the base path cannot be determined by the path alone, the
374
   * default user directory is returned. This is necessary for the creation
375
   * of new files.
376
   *
377
   * @return Path to the directory containing a file being edited, or the
378
   * default user directory if the base path cannot be determined.
379
   */
380
  public Path getBaseDir() {
381
    final var path = getSourcePath().toAbsolutePath().getParent();
382
    return path == null ? DEFAULT_DIRECTORY : path;
383
  }
384
385
  FileType getSourceFileType() {
386
    return lookup( getSourcePath() );
387
  }
388
389
  public Path getThemeDir() {
390
    return mMutator.mThemeDir.get();
391
  }
392
393
  public Path getImageDir() {
394
    return mMutator.mImageDir.get();
395
  }
396
397
  public Path getCacheDir() {
398
    return mMutator.mCacheDir.get();
399
  }
400
401
  public Iterable<String> getImageOrder() {
402
    assert mMutator.mImageOrder != null;
403
404
    final var order = mMutator.mImageOrder.get();
405
    final var token = order.contains( "," ) ? ',' : ' ';
406
407
    return Splitter.on( token ).split( token + order );
408
  }
409
410
  public String getImageServer() {
411
    return mMutator.mImageServer.get();
412
  }
413
414
  public Path getFontDir() {
415
    return mMutator.mFontDir.get();
416
  }
417
418
  public String getEnableMode() {
419
    final var processor = new VariableProcessor( IDENTITY, this );
420
    final var needles = processor.getDefinitions();
421
    final var haystack = mMutator.mEnableMode.get();
422
    final var result = replace( haystack, needles );
423
424
    // If no replacement was made, then the mode variable isn't set.
425
    return result.equals( haystack ) ? "" : result;
36
import static com.keenwrite.util.Strings.sanitize;
37
38
/**
39
 * Provides a context for configuring a chain of {@link Processor} instances.
40
 */
41
public final class ProcessorContext {
42
43
  private final Mutator mMutator;
44
45
  /**
46
   * Determines the file type from the path extension. This should only be
47
   * called when it is known that the file type won't be a definition file
48
   * (e.g., YAML or other definition source), but rather an editable file
49
   * (e.g., Markdown, R Markdown, etc.).
50
   *
51
   * @param path The path with a file name extension.
52
   * @return The FileType for the given path.
53
   */
54
  private static FileType lookup( final Path path ) {
55
    assert path != null;
56
57
    final var prefix = GLOB_PREFIX_FILE;
58
    final var keys = sSettings.getKeys( prefix );
59
60
    var found = false;
61
    var fileType = UNKNOWN;
62
63
    while( keys.hasNext() && !found ) {
64
      final var key = keys.next();
65
      final var patterns = sSettings.getStringSettingList( key );
66
      final var predicate = createFileTypePredicate( patterns );
67
68
      if( predicate.test( toFile( path ) ) ) {
69
        // Remove the EXTENSIONS_PREFIX to get the file name extension mapped
70
        // to a standard name (as defined in the settings.properties file).
71
        final String suffix = key.replace( prefix + '.', "" );
72
        fileType = FileType.from( suffix );
73
        found = true;
74
      }
75
    }
76
77
    return fileType;
78
  }
79
80
  public boolean isExportFormat( final ExportFormat exportFormat ) {
81
    return mMutator.mExportFormat == exportFormat;
82
  }
83
84
  /**
85
   * Responsible for populating the instance variables required by the
86
   * context.
87
   */
88
  public static class Mutator {
89
    private Path mSourcePath;
90
    private Path mTargetPath;
91
    private ExportFormat mExportFormat;
92
    private Supplier<Boolean> mConcatenate = () -> true;
93
    private Supplier<String> mChapters = () -> "";
94
95
    private Supplier<Path> mThemeDir = USER_DIRECTORY::toPath;
96
    private Supplier<Locale> mLocale = () -> Locale.ENGLISH;
97
98
    private Supplier<Map<String, String>> mDefinitions = HashMap::new;
99
    private Supplier<Map<String, String>> mMetadata = HashMap::new;
100
    private Supplier<Caret> mCaret = () -> Caret.builder().build();
101
102
    private Supplier<Path> mImageDir = USER_DIRECTORY::toPath;
103
    private Supplier<String> mImageServer = () -> DIAGRAM_SERVER_NAME;
104
    private Supplier<String> mImageOrder = () -> PERSIST_IMAGES_DEFAULT;
105
    private Supplier<Path> mCacheDir = USER_CACHE_DIR::toPath;
106
    private Supplier<Path> mFontDir = () -> getFontDirectory().toPath();
107
108
    private Supplier<String> mModesEnabled = () -> "";
109
110
    private Supplier<String> mSigilBegan = () -> DEF_DELIM_BEGAN_DEFAULT;
111
    private Supplier<String> mSigilEnded = () -> DEF_DELIM_ENDED_DEFAULT;
112
113
    private Supplier<Path> mRWorkingDir = USER_DIRECTORY::toPath;
114
    private Supplier<String> mRScript = () -> "";
115
116
    private Supplier<Boolean> mCurlQuotes = () -> true;
117
    private Supplier<Boolean> mAutoRemove = () -> true;
118
119
    public void setSourcePath( final Path sourcePath ) {
120
      assert sourcePath != null;
121
      mSourcePath = sourcePath;
122
    }
123
124
    public void setTargetPath( final Path outputPath ) {
125
      assert outputPath != null;
126
      mTargetPath = outputPath;
127
    }
128
129
    public void setThemeDir( final Supplier<Path> themeDir ) {
130
      assert themeDir != null;
131
      mThemeDir = themeDir;
132
    }
133
134
    public void setCacheDir( final Supplier<File> cacheDir ) {
135
      assert cacheDir != null;
136
137
      mCacheDir = () -> {
138
        final var dir = cacheDir.get();
139
140
        return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
141
      };
142
    }
143
144
    public void setImageDir( final Supplier<File> imageDir ) {
145
      assert imageDir != null;
146
147
      mImageDir = () -> {
148
        final var dir = imageDir.get();
149
150
        return (dir == null ? USER_DIRECTORY : dir).toPath();
151
      };
152
    }
153
154
    public void setImageOrder( final Supplier<String> imageOrder ) {
155
      assert imageOrder != null;
156
      mImageOrder = imageOrder;
157
    }
158
159
    public void setImageServer( final Supplier<String> imageServer ) {
160
      assert imageServer != null;
161
      mImageServer = imageServer;
162
    }
163
164
    public void setFontDir( final Supplier<File> fontDir ) {
165
      assert fontDir != null;
166
167
      mFontDir = () -> {
168
        final var dir = fontDir.get();
169
170
        return (dir == null ? USER_DIRECTORY : dir).toPath();
171
      };
172
    }
173
174
    public void setModesEnabled( final Supplier<String> modesEnabled ) {
175
      assert modesEnabled != null;
176
      mModesEnabled = modesEnabled;
177
    }
178
179
    public void setExportFormat( final ExportFormat exportFormat ) {
180
      assert exportFormat != null;
181
      mExportFormat = exportFormat;
182
    }
183
184
    public void setConcatenate( final Supplier<Boolean> concatenate ) {
185
      mConcatenate = concatenate;
186
    }
187
188
    public void setChapters( final Supplier<String> chapters ) {
189
      mChapters = chapters;
190
    }
191
192
    public void setLocale( final Supplier<Locale> locale ) {
193
      assert locale != null;
194
      mLocale = locale;
195
    }
196
197
    /**
198
     * Sets the list of fully interpolated key-value pairs to use when
199
     * substituting variable names back into the document as variable values.
200
     * This uses a {@link Callable} reference so that GUI and command-line
201
     * usage can insert their respective behaviours. That is, this method
202
     * prevents coupling the GUI to the CLI.
203
     *
204
     * @param supplier Defines how to retrieve the definitions.
205
     */
206
    public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
207
      assert supplier != null;
208
      mDefinitions = supplier;
209
    }
210
211
    /**
212
     * Sets metadata to use in the document header. These are made available
213
     * to the typesetting engine as {@code \documentvariable} values.
214
     *
215
     * @param metadata The key/value pairs to publish as document metadata.
216
     */
217
    public void setMetadata( final Supplier<Map<String, String>> metadata ) {
218
      assert metadata != null;
219
      mMetadata = metadata.get() == null ? HashMap::new : metadata;
220
    }
221
222
    /**
223
     * Sets document variables to use when building the document. These
224
     * variables will override existing key/value pairs, or be added as
225
     * new key/value pairs if not already defined. This allows users to
226
     * inject variables into the document from the command-line, allowing
227
     * for dynamic assignment of in-text values when building documents.
228
     *
229
     * @param overrides The key/value pairs to add (or override) as variables.
230
     */
231
    public void setOverrides( final Supplier<Map<String, String>> overrides ) {
232
      assert overrides != null;
233
      assert mDefinitions != null;
234
      assert mDefinitions.get() != null;
235
236
      final var map = overrides.get();
237
238
      if( map != null ) {
239
        mDefinitions.get().putAll( map );
240
      }
241
    }
242
243
    /**
244
     * Sets the source for deriving the {@link Caret}. Typically, this is
245
     * the text editor that has focus.
246
     *
247
     * @param caret The source for the currently active caret.
248
     */
249
    public void setCaret( final Supplier<Caret> caret ) {
250
      assert caret != null;
251
      mCaret = caret;
252
    }
253
254
    public void setSigilBegan( final Supplier<String> sigilBegan ) {
255
      assert sigilBegan != null;
256
      mSigilBegan = sigilBegan;
257
    }
258
259
    public void setSigilEnded( final Supplier<String> sigilEnded ) {
260
      assert sigilEnded != null;
261
      mSigilEnded = sigilEnded;
262
    }
263
264
    public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
265
      assert rWorkingDir != null;
266
      mRWorkingDir = rWorkingDir;
267
    }
268
269
    public void setRScript( final Supplier<String> rScript ) {
270
      assert rScript != null;
271
      mRScript = rScript;
272
    }
273
274
    public void setCurlQuotes( final Supplier<Boolean> curlQuotes ) {
275
      assert curlQuotes != null;
276
      mCurlQuotes = curlQuotes;
277
    }
278
279
    public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
280
      assert autoRemove != null;
281
      mAutoRemove = autoRemove;
282
    }
283
284
    private boolean isExportFormat( final ExportFormat format ) {
285
      return mExportFormat == format;
286
    }
287
  }
288
289
  public static GenericBuilder<Mutator, ProcessorContext> builder() {
290
    return GenericBuilder.of( Mutator::new, ProcessorContext::new );
291
  }
292
293
  /**
294
   * Creates a new context for use by the {@link ProcessorFactory} when
295
   * instantiating new {@link Processor} instances. Although all the
296
   * parameters are required, not all {@link Processor} instances will use
297
   * all parameters.
298
   */
299
  private ProcessorContext( final Mutator mutator ) {
300
    assert mutator != null;
301
302
    mMutator = mutator;
303
  }
304
305
  public Path getSourcePath() {
306
    return mMutator.mSourcePath;
307
  }
308
309
  /**
310
   * Answers what type of input document is to be processed.
311
   *
312
   * @return The input document's {@link MediaType}.
313
   */
314
  public MediaType getSourceType() {
315
    return MediaTypeExtension.fromPath( mMutator.mSourcePath );
316
  }
317
318
  /**
319
   * Fully qualified file name to use when exporting (e.g., document.pdf).
320
   *
321
   * @return Full path to a file name.
322
   */
323
  public Path getTargetPath() {
324
    return mMutator.mTargetPath;
325
  }
326
327
  public ExportFormat getExportFormat() {
328
    return mMutator.mExportFormat;
329
  }
330
331
  public Locale getLocale() {
332
    return mMutator.mLocale.get();
333
  }
334
335
  /**
336
   * Returns the variable map of definitions, without interpolation.
337
   *
338
   * @return A map to help dereference variables.
339
   */
340
  public Map<String, String> getDefinitions() {
341
    return mMutator.mDefinitions.get();
342
  }
343
344
  /**
345
   * Returns the variable map of definitions, with interpolation.
346
   *
347
   * @return A map to help dereference variables.
348
   */
349
  public InterpolatingMap getInterpolatedDefinitions() {
350
    return new InterpolatingMap(
351
      createDefinitionKeyOperator(), getDefinitions()
352
    ).interpolate();
353
  }
354
355
  public Map<String, String> getMetadata() {
356
    return mMutator.mMetadata.get();
357
  }
358
359
  /**
360
   * Returns the current caret position in the document being edited and is
361
   * always up-to-date.
362
   *
363
   * @return Caret position in the document.
364
   */
365
  public Supplier<Caret> getCaret() {
366
    return mMutator.mCaret;
367
  }
368
369
  /**
370
   * Returns the directory that contains the file being edited. When
371
   * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
372
   * {@code null}. This will get absolute path to the file before trying to
373
   * get te parent path, which should always be a valid path. In the unlikely
374
   * event that the base path cannot be determined by the path alone, the
375
   * default user directory is returned. This is necessary for the creation
376
   * of new files.
377
   *
378
   * @return Path to the directory containing a file being edited, or the
379
   * default user directory if the base path cannot be determined.
380
   */
381
  public Path getBaseDir() {
382
    final var path = getSourcePath().toAbsolutePath().getParent();
383
    return path == null ? DEFAULT_DIRECTORY : path;
384
  }
385
386
  FileType getSourceFileType() {
387
    return lookup( getSourcePath() );
388
  }
389
390
  public Path getThemeDir() {
391
    return mMutator.mThemeDir.get();
392
  }
393
394
  public Path getImageDir() {
395
    return mMutator.mImageDir.get();
396
  }
397
398
  public Path getCacheDir() {
399
    return mMutator.mCacheDir.get();
400
  }
401
402
  public Iterable<String> getImageOrder() {
403
    assert mMutator.mImageOrder != null;
404
405
    final var order = mMutator.mImageOrder.get();
406
    final var token = order.contains( "," ) ? ',' : ' ';
407
408
    return Splitter.on( token ).split( token + order );
409
  }
410
411
  public String getImageServer() {
412
    return mMutator.mImageServer.get();
413
  }
414
415
  public Path getFontDir() {
416
    return mMutator.mFontDir.get();
417
  }
418
419
  public String getModesEnabled() {
420
    // Force the processor to select particular sigils.
421
    final var processor = new VariableProcessor( IDENTITY, this );
422
    final var needles = processor.getDefinitions();
423
    final var haystack = sanitize( mMutator.mModesEnabled.get() );
424
425
    return needles.containsKey( haystack )
426
      ? replace( haystack, needles )
427
      : processor.hasSigils( haystack )
428
      ? ""
429
      : haystack;
426430
  }
427431
M src/main/java/com/keenwrite/processors/VariableProcessor.java
22
package com.keenwrite.processors;
33
4
import com.keenwrite.sigils.SigilKeyOperator;
5
46
import java.util.HashMap;
57
import java.util.Map;
68
import java.util.function.Function;
7
import java.util.function.UnaryOperator;
89
910
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
...
1819
1920
  private final ProcessorContext mContext;
20
  private final UnaryOperator<String> mSigilOperator;
21
  private final SigilKeyOperator mSigilOperator;
2122
2223
  /**
...
4243
   * @return An operator for transforming key names.
4344
   */
44
  protected UnaryOperator<String> createKeyOperator(
45
  protected SigilKeyOperator createKeyOperator(
4546
    final ProcessorContext context ) {
4647
    return context.createKeyOperator();
...
7778
  protected String processValue( final String value ) {
7879
    return value;
80
  }
81
82
  /**
83
   * Answers whether the given key is wrapped in sigil tokens.
84
   *
85
   * @param key The key to analyze.
86
   * @return {@code true} if the key is wrapped in sigils.
87
   */
88
  public boolean hasSigils( final String key ) {
89
    return mSigilOperator.match( key ).find();
7990
  }
8091
M src/main/java/com/keenwrite/processors/r/RVariableProcessor.java
66
import com.keenwrite.processors.VariableProcessor;
77
import com.keenwrite.sigils.RKeyOperator;
8
import com.keenwrite.sigils.SigilKeyOperator;
89
910
import java.util.function.UnaryOperator;
...
2122
2223
  @Override
23
  protected UnaryOperator<String> createKeyOperator(
24
  protected SigilKeyOperator createKeyOperator(
2425
    final ProcessorContext context ) {
2526
    return new RKeyOperator();
M src/main/java/com/keenwrite/sigils/RKeyOperator.java
44
import com.keenwrite.collections.BoundedCache;
55
6
import java.util.function.UnaryOperator;
7
86
/**
97
 * Converts dot-separated variable names into names compatible with R. That is,
108
 * {@code variable.name.qualified} becomes {@code v$variable$name$qualified}.
119
 */
12
public final class RKeyOperator implements UnaryOperator<String> {
10
public final class RKeyOperator extends SigilKeyOperator {
1311
  private static final char KEY_SEPARATOR_DEF = '.';
1412
  private static final char KEY_SEPARATOR_R = '$';
...
2624
   * names into R's dollar-symbol-separated names.
2725
   */
28
  public RKeyOperator() { }
26
  public RKeyOperator() {
27
    // The keys are not delimited.
28
    super( "", "" );
29
  }
2930
3031
  /**
...
4445
  public String apply( final String key ) {
4546
    assert key != null;
46
    assert key.length() > 0;
4747
    assert !key.isBlank();
4848
M src/main/java/com/keenwrite/typesetting/Typesetter.java
4646
    private Path mCacheDir = USER_CACHE_DIR.toPath();
4747
    private Path mFontDir = getFontDirectory().toPath();
48
    private String mEnableMode = "";
48
    private String mModesEnabled = "";
4949
    private boolean mAutoRemove;
5050
...
9292
    }
9393
94
    public void setEnableMode( final String enableMode ) {
95
      mEnableMode = enableMode;
94
    public void setModesEnabled( final String modesEnabled ) {
95
      mModesEnabled = modesEnabled;
9696
    }
9797
...
128128
    }
129129
130
    public String getEnableMode() {
131
      return mEnableMode;
130
    public String getModesEnabled() {
131
      return mModesEnabled;
132132
    }
133133
...
195195
    args.add( sourcePath );
196196
197
    final var enableMode = getEnableMode();
197
    final var modesEnabled = getModesEnabled();
198198
199
    if( !enableMode.isBlank() ) {
200
      args.add( format( "--mode=%s", enableMode ) );
199
    if( !modesEnabled.isBlank() ) {
200
      args.add( format( "--mode=%s", modesEnabled ) );
201201
    }
202202
...
242242
  }
243243
244
  protected String getEnableMode() {
245
    return mMutator.getEnableMode();
244
  protected String getModesEnabled() {
245
    return mMutator.getModesEnabled();
246246
  }
247247
M src/main/resources/lexicons/en.txt
Binary file