Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
BUILD.md
Download and install the following software packages:
-* [JDK 22](https://bell-sw.com/pages/downloads) (Full JDK + JavaFX)
-* [Gradle 8.9](https://gradle.org/releases)
-* [Git 2.45](https://git-scm.com/downloads)
+* [JDK 23](https://bell-sw.com/pages/downloads) (Full JDK + JavaFX)
+* [Gradle 8.10.2](https://gradle.org/releases)
+* [Git 2.46.2](https://git-scm.com/downloads)
* [warp v0.4.0-alpha](https://github.com/Reisz/warp/releases/tag/v0.4.0)
keenwrite.sh
java \
-Dprism.order=sw \
- --enable-preview \
--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED \
--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED \
libs/keenquotes.jar
Binary files differ
src/main/java/com/keenwrite/MainPane.java
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
import static java.awt.Desktop.getDesktop;
-import static java.util.concurrent.Executors.newFixedThreadPool;
-import static java.util.concurrent.Executors.newScheduledThreadPool;
-import static java.util.concurrent.TimeUnit.SECONDS;
-import static java.util.stream.Collectors.groupingBy;
-import static javafx.application.Platform.exit;
-import static javafx.application.Platform.runLater;
-import static javafx.scene.control.ButtonType.NO;
-import static javafx.scene.control.ButtonType.YES;
-import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCode.SPACE;
-import static javafx.scene.input.KeyCombination.ALT_DOWN;
-import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
-import static javafx.util.Duration.millis;
-import static javax.swing.SwingUtilities.invokeLater;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-
-/**
- * Responsible for wiring together the main application components for a
- * particular {@link Workspace} (project). These include the definition views,
- * text editors, and preview pane along with any corresponding controllers.
- */
-public final class MainPane extends SplitPane {
-
- private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
- private static final Notifier sNotifier = Services.load( Notifier.class );
-
- /**
- * Used when opening files to determine how each file should be binned and
- * therefore what tab pane to be opened within.
- */
- private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
- TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED
- );
-
- private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 );
- private final AtomicReference<ScheduledFuture<?>> mSaveTask =
- new AtomicReference<>();
-
- /**
- * Prevents re-instantiation of processing classes.
- */
- private final Map<TextResource, Processor<String>> mProcessors =
- new HashMap<>();
-
- private final Workspace mWorkspace;
-
- /**
- * Groups similar file type tabs together.
- */
- private final List<TabPane> mTabPanes = new ArrayList<>();
-
- /**
- * Renders the actively selected plain text editor tab.
- */
- private final HtmlPreview mPreview;
-
- /**
- * Provides an interactive document outline.
- */
- private final DocumentOutline mOutline = new DocumentOutline();
-
- /**
- * Changing the active editor fires the value changed event. This allows
- * refreshes to happen when external definitions are modified and need to
- * trigger the processing chain.
- */
- private final ObjectProperty<TextEditor> mTextEditor =
- new SimpleObjectProperty<>();
-
- /**
- * Changing the active definition editor fires the value changed event. This
- * allows refreshes to happen when external definitions are modified and need
- * to trigger the processing chain.
- */
- private final ObjectProperty<TextDefinition> mDefinitionEditor =
- new SimpleObjectProperty<>();
-
- private final ObjectProperty<SpellChecker> mSpellChecker;
-
- private final TextEditorSpellChecker mEditorSpeller;
-
- /**
- * Called when the definition data is changed.
- */
- private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
- _ -> {
- process( getTextEditor() );
- save( getTextDefinition() );
- };
-
- /**
- * Tracks the number of detached tab panels opened into their own windows,
- * which allows unique identification of subordinate windows by their title.
- * It is doubtful more than 128 windows, much less 256, will be created.
- */
- private byte mWindowCount;
-
- private final VariableNameInjector mVariableNameInjector;
-
- private final RBootstrapController mRBootstrapController;
-
- private final DocumentStatistics mStatistics;
-
- @SuppressWarnings( { "FieldCanBeLocal", "unused" } )
- private final TypesetterInstaller mInstallWizard;
-
- /**
- * Adds all content panels to the main user interface. This will load the
- * configuration settings from the workspace to reproduce the settings from
- * a previous session.
- */
- public MainPane( final Workspace workspace ) {
- mWorkspace = workspace;
- mSpellChecker = createSpellChecker();
- mEditorSpeller = createTextEditorSpellChecker( mSpellChecker );
- mPreview = new HtmlPreview( workspace );
- mStatistics = new DocumentStatistics( workspace );
-
- mTextEditor.addListener( ( _, o, n ) -> {
- if( o != null ) {
- removeProcessor( o );
- }
-
- if( n != null ) {
- mPreview.setBaseUri( n.getPath() );
- updateProcessors( n );
- process( n );
- }
- } );
-
- mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
- mDefinitionEditor.set( createDefinitionEditor( workspace ) );
- mVariableNameInjector = new VariableNameInjector( workspace );
- mRBootstrapController = new RBootstrapController(
- workspace, mDefinitionEditor.get()::getDefinitions
- );
-
- // If the user modifies the definitions, re-process the variables.
- mDefinitionEditor.addListener( ( _, _, _ ) -> {
- final var textEditor = getTextEditor();
-
- if( textEditor.isMediaType( TEXT_R_MARKDOWN ) ) {
- mRBootstrapController.update();
- }
-
- process( textEditor );
- } );
-
- open( collect( getRecentFiles() ) );
- viewPreview();
- setDividerPositions( calculateDividerPositions() );
-
- // Once the main scene's window regains focus, update the active definition
- // editor to the currently selected tab.
- runLater( () -> getWindow().setOnCloseRequest( event -> {
- // Order matters: Open file names must be persisted before closing all.
- mWorkspace.save();
-
- if( closeAll() ) {
- exit();
- }
-
- event.consume();
- } ) );
-
- register( this );
- initAutosave( workspace );
-
- restoreSession();
- runLater( this::restoreFocus );
-
- mInstallWizard = new TypesetterInstaller( workspace );
- }
-
- /**
- * Called when spellchecking can be run. This will reload the dictionary
- * into memory once, and then re-use it for all the existing text editors.
- *
- * @param event The event to process, having a populated word-frequency map.
- */
- @Subscribe
- public void handle( final LexiconLoadedEvent event ) {
- final var lexicon = event.getLexicon();
-
- try {
- final var checker = SymSpellSpeller.forLexicon( lexicon );
- mSpellChecker.set( checker );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- @Subscribe
- public void handle( final TextEditorFocusEvent event ) {
- mTextEditor.set( event.get() );
- }
-
- @Subscribe
- public void handle( final TextDefinitionFocusEvent event ) {
- mDefinitionEditor.set( event.get() );
- }
-
- /**
- * Typically called when a file name is clicked in the preview panel.
- *
- * @param event The event to process, must contain a valid file reference.
- */
- @Subscribe
- public void handle( final FileOpenEvent event ) {
- final File eventFile;
- final var eventUri = event.getUri();
-
- if( eventUri.isAbsolute() ) {
- eventFile = new File( eventUri.getPath() );
- }
- else {
- final var activeFile = getTextEditor().getFile();
- final var parent = activeFile.getParentFile();
-
- if( parent == null ) {
- clue( new FileNotFoundException( eventUri.getPath() ) );
- return;
- }
- else {
- final var parentPath = parent.getAbsolutePath();
- eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) );
- }
- }
-
- final var mediaType = MediaTypeExtension.fromFile( eventFile );
-
- runLater( () -> {
- // Open text files locally.
- if( mediaType.isType( TEXT ) ) {
- open( eventFile );
- }
- else {
- try {
- // Delegate opening all other file types to the operating system.
- getDesktop().open( eventFile );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
- } );
- }
-
- @Subscribe
- public void handle( final CaretNavigationEvent event ) {
- runLater( () -> {
- final var textArea = getTextEditor();
- textArea.moveTo( event.getOffset() );
- textArea.requestFocus();
- } );
- }
-
- @Subscribe
- public void handle( final InsertDefinitionEvent<String> event ) {
- final var leaf = event.getLeaf();
- final var editor = mTextEditor.get();
-
- mVariableNameInjector.insert( editor, leaf );
- }
-
- private void initAutosave( final Workspace workspace ) {
- final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE );
-
- rate.addListener(
- ( _, _, _ ) -> {
- final var taskRef = mSaveTask.get();
-
- // Prevent multiple auto-saves from running.
- if( taskRef != null ) {
- taskRef.cancel( false );
- }
-
- initAutosave( rate );
- }
- );
-
- // Start the save listener (avoids duplicating some code).
- initAutosave( rate );
- }
-
- private void initAutosave( final IntegerProperty rate ) {
- mSaveTask.set(
- mSaver.scheduleAtFixedRate(
- () -> {
- if( getTextEditor().isModified() ) {
- // Ensure the modified indicator is cleared by running on EDT.
- runLater( this::save );
- }
- }, 0, rate.intValue(), SECONDS
- )
- );
- }
-
- /**
- * TODO: Load divider positions from exported settings, see
- * {@link #collect(SetProperty)} comment.
- */
- private double[] calculateDividerPositions() {
- final var ratio = 100f / getItems().size() / 100;
- final var positions = getDividerPositions();
-
- for( int i = 0; i < positions.length; i++ ) {
- positions[ i ] = ratio * i;
- }
-
- return positions;
- }
-
- /**
- * Opens all the files into the application, provided the paths are unique.
- * This may only be called for any type of files that a user can edit
- * (i.e., update and persist), such as definitions and text files.
- *
- * @param files The list of files to open.
- */
- public void open( final List<File> files ) {
- files.forEach( this::open );
- }
-
- /**
- * This opens the given file. Since the preview pane is not a file that
- * can be opened, it is safe to add a listener to the detachable pane.
- * This will exit early if the given file is not a regular file (i.e., a
- * directory).
- *
- * @param inputFile The file to open.
- */
- private void open( final File inputFile ) {
- // Prevent opening directories (a non-existent "untitled.md" is fine).
- if( !inputFile.isFile() && inputFile.exists() ) {
- return;
- }
-
- final var mediaType = fromFilename( inputFile );
-
- // Only allow opening text files.
- if( !mediaType.isType( TEXT ) ) {
- return;
- }
-
- final var tab = createTab( inputFile );
- final var node = tab.getContent();
- final var tabPane = obtainTabPane( mediaType );
-
- tab.setTooltip( createTooltip( inputFile ) );
- tabPane.setFocusTraversable( false );
- tabPane.setTabClosingPolicy( ALL_TABS );
- tabPane.getTabs().add( tab );
-
- // Attach the tab scene factory for new tab panes.
- if( !getItems().contains( tabPane ) ) {
- addTabPane(
- node instanceof TextDefinition ? 0 : getItems().size(), tabPane
- );
- }
-
- if( inputFile.isFile() ) {
- getRecentFiles().add( inputFile.getAbsolutePath() );
-
- final var dir = inputFile.getParentFile();
- mWorkspace.fileProperty( KEY_UI_RECENT_DIR ).setValue( dir );
- }
- }
-
- /**
- * Gives focus to the most recently edited document and attempts to move
- * the caret to the most recently known offset into said document.
- */
- private void restoreSession() {
- final var workspace = getWorkspace();
- final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT );
- final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET );
-
- for( final var pane : mTabPanes ) {
- for( final var tab : pane.getTabs() ) {
- final var tooltip = tab.getTooltip();
-
- if( tooltip != null ) {
- final var tabName = tooltip.getText();
- final var fileName = file.get().toString();
-
- if( tabName.equalsIgnoreCase( fileName ) ) {
- final var node = tab.getContent();
-
- pane.getSelectionModel().select( tab );
- node.requestFocus();
-
- if( node instanceof TextEditor editor ) {
- runLater( () -> editor.moveTo( offset.getValue() ) );
- }
-
- break;
- }
- }
- }
- }
- }
-
- /**
- * Sets the focus to the middle pane, which contains the text editor tabs.
- */
- private void restoreFocus() {
- // Work around a bug where focusing directly on the middle pane results
- // in the R engine not loading variables properly.
- mTabPanes.get( 0 ).requestFocus();
-
- // This is the only line that should be required.
- mTabPanes.get( 1 ).requestFocus();
- }
-
- /**
- * Opens a new text editor document using a document file name that doesn't
- * clash with an existing document.
- */
- public void newTextEditor() {
- final String key = "file.default.document.";
- final String prefix = Constants.get( String.format( "%s%s", key, "prefix" ) );
- final String suffix = Constants.get( String.format( "%s%s", key, "suffix" ) );
-
- File file = new File( String.format( "%s.%s", prefix, suffix ) );
- int i = 0;
-
- while( file.exists() && i++ < 100 ) {
- file = new File( String.format( "%s-%s.%s", prefix, i, suffix ) );
+import static java.lang.String.*;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import static java.util.concurrent.Executors.newScheduledThreadPool;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.stream.Collectors.groupingBy;
+import static javafx.application.Platform.exit;
+import static javafx.application.Platform.runLater;
+import static javafx.scene.control.ButtonType.NO;
+import static javafx.scene.control.ButtonType.YES;
+import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyCode.SPACE;
+import static javafx.scene.input.KeyCombination.ALT_DOWN;
+import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
+import static javafx.util.Duration.millis;
+import static javax.swing.SwingUtilities.invokeLater;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
+
+/**
+ * Responsible for wiring together the main application components for a
+ * particular {@link Workspace} (project). These include the definition views,
+ * text editors, and preview pane along with any corresponding controllers.
+ */
+public final class MainPane extends SplitPane {
+
+ private static final ExecutorService sExecutor = newFixedThreadPool( 1 );
+ private static final Notifier sNotifier = Services.load( Notifier.class );
+
+ /**
+ * Used when opening files to determine how each file should be binned and
+ * therefore what tab pane to be opened within.
+ */
+ private static final Set<MediaType> PLAIN_TEXT_FORMAT = Set.of(
+ TEXT_MARKDOWN, TEXT_R_MARKDOWN, UNDEFINED
+ );
+
+ private final ScheduledExecutorService mSaver = newScheduledThreadPool( 1 );
+ private final AtomicReference<ScheduledFuture<?>> mSaveTask =
+ new AtomicReference<>();
+
+ /**
+ * Prevents re-instantiation of processing classes.
+ */
+ private final Map<TextResource, Processor<String>> mProcessors =
+ new HashMap<>();
+
+ private final Workspace mWorkspace;
+
+ /**
+ * Groups similar file type tabs together.
+ */
+ private final List<TabPane> mTabPanes = new ArrayList<>();
+
+ /**
+ * Renders the actively selected plain text editor tab.
+ */
+ private final HtmlPreview mPreview;
+
+ /**
+ * Provides an interactive document outline.
+ */
+ private final DocumentOutline mOutline = new DocumentOutline();
+
+ /**
+ * Changing the active editor fires the value changed event. This allows
+ * refreshes to happen when external definitions are modified and need to
+ * trigger the processing chain.
+ */
+ private final ObjectProperty<TextEditor> mTextEditor =
+ new SimpleObjectProperty<>();
+
+ /**
+ * Changing the active definition editor fires the value changed event. This
+ * allows refreshes to happen when external definitions are modified and need
+ * to trigger the processing chain.
+ */
+ private final ObjectProperty<TextDefinition> mDefinitionEditor =
+ new SimpleObjectProperty<>();
+
+ private final ObjectProperty<SpellChecker> mSpellChecker;
+
+ private final TextEditorSpellChecker mEditorSpeller;
+
+ /**
+ * Called when the definition data is changed.
+ */
+ private final EventHandler<TreeModificationEvent<Event>> mTreeHandler =
+ _ -> {
+ process( getTextEditor() );
+ save( getTextDefinition() );
+ };
+
+ /**
+ * Tracks the number of detached tab panels opened into their own windows,
+ * which allows unique identification of subordinate windows by their title.
+ * It is doubtful more than 128 windows, much less 256, will be created.
+ */
+ private byte mWindowCount;
+
+ private final VariableNameInjector mVariableNameInjector;
+
+ private final RBootstrapController mRBootstrapController;
+
+ private final DocumentStatistics mStatistics;
+
+ @SuppressWarnings( { "FieldCanBeLocal", "unused" } )
+ private final TypesetterInstaller mInstallWizard;
+
+ /**
+ * Adds all content panels to the main user interface. This will load the
+ * configuration settings from the workspace to reproduce the settings from
+ * a previous session.
+ */
+ public MainPane( final Workspace workspace ) {
+ mWorkspace = workspace;
+ mSpellChecker = createSpellChecker();
+ mEditorSpeller = createTextEditorSpellChecker( mSpellChecker );
+ mPreview = new HtmlPreview( workspace );
+ mStatistics = new DocumentStatistics( workspace );
+
+ mTextEditor.addListener( ( _, o, n ) -> {
+ if( o != null ) {
+ removeProcessor( o );
+ }
+
+ if( n != null ) {
+ mPreview.setBaseUri( n.getPath() );
+ updateProcessors( n );
+ process( n );
+ }
+ } );
+
+ mTextEditor.set( createMarkdownEditor( DOCUMENT_DEFAULT ) );
+ mDefinitionEditor.set( createDefinitionEditor( workspace ) );
+ mVariableNameInjector = new VariableNameInjector( workspace );
+ mRBootstrapController = new RBootstrapController(
+ workspace, mDefinitionEditor.get()::getDefinitions
+ );
+
+ // If the user modifies the definitions, re-process the variables.
+ mDefinitionEditor.addListener( ( _, _, _ ) -> {
+ final var textEditor = getTextEditor();
+
+ if( textEditor.isMediaType( TEXT_R_MARKDOWN ) ) {
+ mRBootstrapController.update();
+ }
+
+ process( textEditor );
+ } );
+
+ open( collect( getRecentFiles() ) );
+ viewPreview();
+ setDividerPositions( calculateDividerPositions() );
+
+ // Once the main scene's window regains focus, update the active definition
+ // editor to the currently selected tab.
+ runLater( () -> getWindow().setOnCloseRequest( event -> {
+ // Order matters: Open file names must be persisted before closing all.
+ mWorkspace.save();
+
+ if( closeAll() ) {
+ exit();
+ }
+
+ event.consume();
+ } ) );
+
+ register( this );
+ initAutosave( workspace );
+
+ restoreSession();
+ runLater( this::restoreFocus );
+
+ mInstallWizard = new TypesetterInstaller( workspace );
+ }
+
+ /**
+ * Called when spellchecking can be run. This will reload the dictionary
+ * into memory once, and then re-use it for all the existing text editors.
+ *
+ * @param event The event to process, having a populated word-frequency map.
+ */
+ @Subscribe
+ public void handle( final LexiconLoadedEvent event ) {
+ final var lexicon = event.getLexicon();
+
+ try {
+ final var checker = SymSpellSpeller.forLexicon( lexicon );
+ mSpellChecker.set( checker );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ @Subscribe
+ public void handle( final TextEditorFocusEvent event ) {
+ mTextEditor.set( event.get() );
+ }
+
+ @Subscribe
+ public void handle( final TextDefinitionFocusEvent event ) {
+ mDefinitionEditor.set( event.get() );
+ }
+
+ /**
+ * Typically called when a file name is clicked in the preview panel.
+ *
+ * @param event The event to process, must contain a valid file reference.
+ */
+ @Subscribe
+ public void handle( final FileOpenEvent event ) {
+ final File eventFile;
+ final var eventUri = event.getUri();
+
+ if( eventUri.isAbsolute() ) {
+ eventFile = new File( eventUri.getPath() );
+ }
+ else {
+ final var activeFile = getTextEditor().getFile();
+ final var parent = activeFile.getParentFile();
+
+ if( parent == null ) {
+ clue( new FileNotFoundException( eventUri.getPath() ) );
+ return;
+ }
+ else {
+ final var parentPath = parent.getAbsolutePath();
+ eventFile = toFile( Path.of( parentPath, eventUri.getPath() ) );
+ }
+ }
+
+ final var mediaType = MediaTypeExtension.fromFile( eventFile );
+
+ runLater( () -> {
+ // Open text files locally.
+ if( mediaType.isType( TEXT ) ) {
+ open( eventFile );
+ }
+ else {
+ try {
+ // Delegate opening all other file types to the operating system.
+ getDesktop().open( eventFile );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+ } );
+ }
+
+ @Subscribe
+ public void handle( final CaretNavigationEvent event ) {
+ runLater( () -> {
+ final var textArea = getTextEditor();
+ textArea.moveTo( event.getOffset() );
+ textArea.requestFocus();
+ } );
+ }
+
+ @Subscribe
+ public void handle( final InsertDefinitionEvent<String> event ) {
+ final var leaf = event.getLeaf();
+ final var editor = mTextEditor.get();
+
+ mVariableNameInjector.insert( editor, leaf );
+ }
+
+ private void initAutosave( final Workspace workspace ) {
+ final var rate = workspace.integerProperty( KEY_EDITOR_AUTOSAVE );
+
+ rate.addListener(
+ ( _, _, _ ) -> {
+ final var taskRef = mSaveTask.get();
+
+ // Prevent multiple auto-saves from running.
+ if( taskRef != null ) {
+ taskRef.cancel( false );
+ }
+
+ initAutosave( rate );
+ }
+ );
+
+ // Start the save listener (avoids duplicating some code).
+ initAutosave( rate );
+ }
+
+ private void initAutosave( final IntegerProperty rate ) {
+ mSaveTask.set(
+ mSaver.scheduleAtFixedRate(
+ () -> {
+ if( getTextEditor().isModified() ) {
+ // Ensure the modified indicator is cleared by running on EDT.
+ runLater( this::save );
+ }
+ }, 0, rate.intValue(), SECONDS
+ )
+ );
+ }
+
+ /**
+ * TODO: Load divider positions from exported settings, see
+ * {@link #collect(SetProperty)} comment.
+ */
+ private double[] calculateDividerPositions() {
+ final var ratio = 100f / getItems().size() / 100;
+ final var positions = getDividerPositions();
+
+ for( int i = 0; i < positions.length; i++ ) {
+ positions[ i ] = ratio * i;
+ }
+
+ return positions;
+ }
+
+ /**
+ * Opens all the files into the application, provided the paths are unique.
+ * This may only be called for any type of files that a user can edit
+ * (i.e., update and persist), such as definitions and text files.
+ *
+ * @param files The list of files to open.
+ */
+ public void open( final List<File> files ) {
+ files.forEach( this::open );
+ }
+
+ /**
+ * This opens the given file. Since the preview pane is not a file that
+ * can be opened, it is safe to add a listener to the detachable pane.
+ * This will exit early if the given file is not a regular file (i.e., a
+ * directory).
+ *
+ * @param inputFile The file to open.
+ */
+ private void open( final File inputFile ) {
+ // Prevent opening directories (a non-existent "untitled.md" is fine).
+ if( !inputFile.isFile() && inputFile.exists() ) {
+ return;
+ }
+
+ final var mediaType = fromFilename( inputFile );
+
+ // Only allow opening text files.
+ if( !mediaType.isType( TEXT ) ) {
+ return;
+ }
+
+ final var tab = createTab( inputFile );
+ final var node = tab.getContent();
+ final var tabPane = obtainTabPane( mediaType );
+
+ tab.setTooltip( createTooltip( inputFile ) );
+ tabPane.setFocusTraversable( false );
+ tabPane.setTabClosingPolicy( ALL_TABS );
+ tabPane.getTabs().add( tab );
+
+ // Attach the tab scene factory for new tab panes.
+ if( !getItems().contains( tabPane ) ) {
+ addTabPane(
+ node instanceof TextDefinition ? 0 : getItems().size(), tabPane
+ );
+ }
+
+ if( inputFile.isFile() ) {
+ getRecentFiles().add( inputFile.getAbsolutePath() );
+
+ final var dir = inputFile.getParentFile();
+ mWorkspace.fileProperty( KEY_UI_RECENT_DIR ).setValue( dir );
+ }
+ }
+
+ /**
+ * Gives focus to the most recently edited document and attempts to move
+ * the caret to the most recently known offset into said document.
+ */
+ private void restoreSession() {
+ final var workspace = getWorkspace();
+ final var file = workspace.fileProperty( KEY_UI_RECENT_DOCUMENT );
+ final var offset = workspace.integerProperty( KEY_UI_RECENT_OFFSET );
+
+ for( final var pane : mTabPanes ) {
+ for( final var tab : pane.getTabs() ) {
+ final var tooltip = tab.getTooltip();
+
+ if( tooltip != null ) {
+ final var tabName = tooltip.getText();
+ final var fileName = file.get().toString();
+
+ if( tabName.equalsIgnoreCase( fileName ) ) {
+ final var node = tab.getContent();
+
+ pane.getSelectionModel().select( tab );
+ node.requestFocus();
+
+ if( node instanceof TextEditor editor ) {
+ runLater( () -> editor.moveTo( offset.getValue() ) );
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the focus to the middle pane, which contains the text editor tabs.
+ */
+ private void restoreFocus() {
+ // Work around a bug where focusing directly on the middle pane results
+ // in the R engine not loading variables properly.
+ mTabPanes.get( 0 ).requestFocus();
+
+ // This is the only line that should be required.
+ mTabPanes.get( 1 ).requestFocus();
+ }
+
+ /**
+ * Opens a new text editor document using a document file name that doesn't
+ * clash with an existing document.
+ */
+ public void newTextEditor() {
+ final String key = "file.default.document.";
+ final String prefix = Constants.get( format( "%s%s", key, "prefix" ) );
+ final String suffix = Constants.get( format( "%s%s", key, "suffix" ) );
+
+ File file = new File( format( "%s.%s", prefix, suffix ) );
+ int i = 0;
+
+ while( file.exists() && i++ < 100 ) {
+ file = new File( format( "%s-%s.%s", prefix, i, suffix ) );
}
src/main/java/com/keenwrite/processors/ProcessorFactory.java
package com.keenwrite.processors;
+import com.keenwrite.processors.html.HtmlProcessor;
import com.keenwrite.processors.html.PreformattedProcessor;
import com.keenwrite.processors.html.XhtmlProcessor;
final var successor = switch( outputType ) {
case NONE -> preview;
+ case HTML_TEX_DELIMITED -> createHtmlProcessor( context );
case XHTML_TEX -> createXhtmlProcessor( context );
case TEXT_TEX -> createTextProcessor( context );
final ProcessorContext context ) {
return createXhtmlProcessor( IDENTITY, context );
- }
-
- private static Processor<String> createTextProcessor(
- final ProcessorContext context ) {
- return new TextProcessor( IDENTITY, context );
}
final var pdfProcessor = new PdfProcessor( context );
return createXhtmlProcessor( pdfProcessor, context );
+ }
+
+ private static Processor<String> createHtmlProcessor(
+ final ProcessorContext context ) {
+ return new HtmlProcessor( IDENTITY, context );
+ }
+
+ private static Processor<String> createTextProcessor(
+ final ProcessorContext context ) {
+ return new TextProcessor( IDENTITY, context );
}
src/main/java/com/keenwrite/processors/html/Configuration.java
+package com.keenwrite.processors.html;
+
+import com.whitemagicsoftware.keenquotes.lex.FilterType;
+import com.whitemagicsoftware.keenquotes.parser.Apostrophe;
+import com.whitemagicsoftware.keenquotes.parser.Contractions;
+import com.whitemagicsoftware.keenquotes.parser.Curler;
+
+import static com.whitemagicsoftware.keenquotes.parser.Contractions.*;
+
+/**
+ * Ensures the contractions aren't created multiple times when creating
+ * a class that changes typographic straight quotes into curly quotes.
+ */
+public final class Configuration {
+ /**
+ * Creates contracts with a custom set of unambiguous English contractions.
+ */
+ private final static Contractions CONTRACTIONS = new Builder().build();
+
+ public static Curler createCurler(
+ final FilterType filterType,
+ final Apostrophe apostrophe ) {
+ return new Curler( CONTRACTIONS, filterType, apostrophe );
+ }
+}
src/main/java/com/keenwrite/processors/html/HtmlProcessor.java
+/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+package com.keenwrite.processors.html;
+
+import com.keenwrite.processors.ExecutorProcessor;
+import com.keenwrite.processors.Processor;
+import com.keenwrite.processors.ProcessorContext;
+import com.whitemagicsoftware.keenquotes.parser.Curler;
+
+import static com.keenwrite.processors.html.Configuration.createCurler;
+import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
+import static com.whitemagicsoftware.keenquotes.parser.Apostrophe.CONVERT_RSQUOTE_HEX;
+
+/**
+ * This is the processor used when an HTML file name extension is encountered.
+ */
+public final class HtmlProcessor extends ExecutorProcessor<String> {
+ private static final Curler CURLER = createCurler(
+ FILTER_XML, CONVERT_RSQUOTE_HEX
+ );
+
+ private final ProcessorContext mContext;
+
+ /**
+ * Constructs an HTML processor capable of curling straight quotes.
+ *
+ * @param successor The next processor in the chain to use for text
+ * processing.
+ */
+ public HtmlProcessor(
+ final Processor<String> successor,
+ final ProcessorContext context ) {
+ super( successor );
+
+ mContext = context;
+ }
+
+ /**
+ * Returns the given string with quotations marks encoded as HTML entities,
+ * provided the user opted to curl quotation marks.
+ *
+ * @param t The string having quotation marks to replace.
+ * @return The string with quotation marks curled.
+ */
+ @Override
+ public String apply( final String t ) {
+ return mContext.getCurlQuotes() ? CURLER.apply( t ) : t;
+ }
+}
src/main/java/com/keenwrite/processors/html/IdentityProcessor.java
-/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. */
package com.keenwrite.processors.html;
* {@code null}).
*/
- private IdentityProcessor() {
- }
+ private IdentityProcessor() {}
/**
src/main/java/com/keenwrite/processors/html/XhtmlProcessor.java
-/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
+/* Copyright 2023-2024 White Magic Software, Ltd. -- All rights reserved.
*
* SPDX-License-Identifier: MIT
import static com.keenwrite.io.SysFile.toFile;
import static com.keenwrite.io.downloads.DownloadManager.open;
+import static com.keenwrite.processors.html.Configuration.createCurler;
import static com.keenwrite.util.ProtocolScheme.getProtocol;
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
+import static com.whitemagicsoftware.keenquotes.parser.Apostrophe.CONVERT_APOS;
import static java.lang.String.format;
import static java.lang.String.valueOf;
*/
public final class XhtmlProcessor extends ExecutorProcessor<String> {
- private static final Curler sCurler =
- new Curler( createContractions(), FILTER_XML, true );
+ private static final Curler CURLER = createCurler( FILTER_XML, CONVERT_APOS );
private final ProcessorContext mContext;
final var curl = mContext.getCurlQuotes();
- return curl ? sCurler.apply( document ) : document;
+ return curl ? CURLER.apply( document ) : document;
} catch( final Exception ex ) {
clue( ex );
src/main/java/com/keenwrite/processors/markdown/BaseMarkdownProcessor.java
import com.keenwrite.processors.markdown.extensions.captions.CaptionExtension;
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
+import com.keenwrite.processors.markdown.extensions.quotes.EscapedQuotesExtension;
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
import com.keenwrite.processors.markdown.extensions.references.CrossReferenceExtension;
extensions.add( CrossReferenceExtension.create() );
extensions.add( CaptionExtension.create() );
+ extensions.add( EscapedQuotesExtension.create() );
return extensions;

Curls quotes in HTML, removes JDK preview, fixes escaped straight quotes

Author DaveJarvis <email>
Date 2024-10-10 00:05:41 GMT-0700
Commit e2d06c1059900ab430ae6337fe7783d9bac8b5c9
Parent c2fb90a
Delta 530 lines added, 445 lines removed, 85-line increase