Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Real-time spellcheck

AuthorDaveJarvis <email>
Date2020-07-05 00:30:47 GMT-0700
Commit3d7e4f29e00dffcb361852aed01134b152ddce52
Parent2221224
build.gradle
if (project.hasProperty('targetOs')) {
- if ("windows".equals(targetOs)) {
+ if ("windows" == targetOs) {
os = ["win"]
- }
- else {
+ } else {
os = [targetOs]
}
implementation 'org.apache.xmlgraphics:batik-util:1.13'
implementation 'org.apache.xmlgraphics:batik-xml:1.13'
+
+ // Spelling implementation
+ implementation fileTree(include: ['**/*.jar'], dir: 'libs')
// Misc.
libs/jsymspell/jsymspell-core-1.0-SNAPSHOT-javadoc.jar
Binary files differ
libs/jsymspell/jsymspell-core-1.0-SNAPSHOT-sources.jar
Binary files differ
libs/jsymspell/jsymspell-core-1.0-SNAPSHOT.jar
Binary files differ
src/main/java/com/scrivenvar/Constants.java
/**
+ * Resource directory where different language lexicons are located.
+ */
+ public static final String LEXICONS_DIRECTORY = "lexicons";
+
+ /**
* Used as the prefix for uniquely identifying HTML block elements, which
* helps coordinate scrolling the preview pane to where the user is typing.
src/main/java/com/scrivenvar/Main.java
final var font = Font.createFont( Font.TRUETYPE_FONT, is );
final Map attributes = font.getAttributes();
+
attributes.put( LIGATURES, LIGATURES_ON );
ge.registerFont( font.deriveFont( attributes ) );
src/main/java/com/scrivenvar/MainWindow.java
import com.scrivenvar.service.Snitch;
import com.scrivenvar.service.events.Notifier;
-import com.scrivenvar.util.Action;
-import com.scrivenvar.util.ActionBuilder;
-import com.scrivenvar.util.ActionUtils;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.property.BooleanProperty;
-import javafx.beans.property.SimpleBooleanProperty;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ListChangeListener.Change;
-import javafx.collections.ObservableList;
-import javafx.event.Event;
-import javafx.event.EventHandler;
-import javafx.geometry.Pos;
-import javafx.scene.Node;
-import javafx.scene.Scene;
-import javafx.scene.control.*;
-import javafx.scene.control.Alert.AlertType;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
-import javafx.scene.input.Clipboard;
-import javafx.scene.input.ClipboardContent;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.layout.BorderPane;
-import javafx.scene.layout.VBox;
-import javafx.scene.text.Text;
-import javafx.stage.Window;
-import javafx.stage.WindowEvent;
-import javafx.util.Duration;
-import org.apache.commons.lang3.SystemUtils;
-import org.controlsfx.control.StatusBar;
-import org.fxmisc.richtext.StyleClassedTextArea;
-import org.reactfx.value.Val;
-import org.xhtmlrenderer.util.XRLog;
-
-import java.nio.file.Path;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Observable;
-import java.util.Observer;
-import java.util.function.Function;
-import java.util.prefs.Preferences;
-
-import static com.scrivenvar.Constants.*;
-import static com.scrivenvar.Messages.get;
-import static com.scrivenvar.util.StageState.*;
-import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
-import static javafx.application.Platform.runLater;
-import static javafx.event.Event.fireEvent;
-import static javafx.scene.input.KeyCode.ENTER;
-import static javafx.scene.input.KeyCode.TAB;
-import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
-
-/**
- * Main window containing a tab pane in the center for file editors.
- */
-public class MainWindow implements Observer {
- /**
- * The {@code OPTIONS} variable must be declared before all other variables
- * to prevent subsequent initializations from failing due to missing user
- * preferences.
- */
- private final static Options OPTIONS = Services.load( Options.class );
- private final static Snitch SNITCH = Services.load( Snitch.class );
- private final static Notifier NOTIFIER = Services.load( Notifier.class );
-
- private final Scene mScene;
- private final StatusBar mStatusBar;
- private final Text mLineNumberText;
- private final TextField mFindTextField;
-
- private final Object mMutex = new Object();
-
- /**
- * Prevents re-instantiation of processing classes.
- */
- private final Map<FileEditorTab, Processor<String>> mProcessors =
- new HashMap<>();
-
- private final Map<String, String> mResolvedMap =
- new HashMap<>( DEFAULT_MAP_SIZE );
-
- /**
- * Called when the definition data is changed.
- */
- private final EventHandler<TreeItem.TreeModificationEvent<Event>>
- mTreeHandler = event -> {
- exportDefinitions( getDefinitionPath() );
- interpolateResolvedMap();
- renderActiveTab();
- };
-
- /**
- * Called to switch to the definition pane when the user presses the TAB key.
- */
- private final EventHandler<? super KeyEvent> mTabKeyHandler =
- (EventHandler<KeyEvent>) event -> {
- if( event.getCode() == TAB ) {
- getDefinitionPane().requestFocus();
- event.consume();
- }
- };
-
- /**
- * Called to inject the selected item when the user presses ENTER in the
- * definition pane.
- */
- private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
- event -> {
- if( event.getCode() == ENTER ) {
- getVariableNameInjector().injectSelectedItem();
- }
- };
-
- private final ChangeListener<Integer> mCaretPositionListener =
- ( observable, oldPosition, newPosition ) -> {
- final FileEditorTab tab = getActiveFileEditorTab();
- final EditorPane pane = tab.getEditorPane();
- final StyleClassedTextArea editor = pane.getEditor();
-
- getLineNumberText().setText(
- get( STATUS_BAR_LINE,
- editor.getCurrentParagraph() + 1,
- editor.getParagraphs().size(),
- editor.getCaretPosition()
- )
- );
- };
-
- private final ChangeListener<Integer> mCaretParagraphListener =
- ( observable, oldIndex, newIndex ) ->
- scrollToParagraph( newIndex, true );
-
- private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
- private final DefinitionPane mDefinitionPane = new DefinitionPane();
- private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
- private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
- mCaretPositionListener,
- mCaretParagraphListener );
-
- /**
- * Listens on the definition pane for double-click events.
- */
- private final VariableNameInjector mVariableNameInjector
- = new VariableNameInjector( mDefinitionPane );
-
- public MainWindow() {
- mStatusBar = createStatusBar();
- mLineNumberText = createLineNumberText();
- mFindTextField = createFindTextField();
- mScene = createScene();
-
- System.getProperties()
- .setProperty( "xr.util-logging.loggingEnabled", "true" );
- XRLog.setLoggingEnabled( true );
-
- initLayout();
- initFindInput();
- initSnitch();
- initDefinitionListener();
- initTabAddedListener();
- initTabChangedListener();
- initPreferences();
- initVariableNameInjector();
-
- NOTIFIER.addObserver( this );
- }
-
- private void initLayout() {
- final Scene appScene = getScene();
-
- appScene.getStylesheets().add( STYLESHEET_SCENE );
-
- // TODO: Apply an XML syntax highlighting for XML files.
-// appScene.getStylesheets().add( STYLESHEET_XML );
- appScene.windowProperty().addListener(
- ( observable, oldWindow, newWindow ) ->
- newWindow.setOnCloseRequest(
- e -> {
- if( !getFileEditorPane().closeAllEditors() ) {
- e.consume();
- }
- }
- )
- );
- }
-
- /**
- * Initialize the find input text field to listen on F3, ENTER, and
- * ESCAPE key presses.
- */
- private void initFindInput() {
- final TextField input = getFindTextField();
-
- input.setOnKeyPressed( ( KeyEvent event ) -> {
- switch( event.getCode() ) {
- case F3:
- case ENTER:
- editFindNext();
- break;
- case F:
- if( !event.isControlDown() ) {
- break;
- }
- case ESCAPE:
- getStatusBar().setGraphic( null );
- getActiveFileEditorTab().getEditorPane().requestFocus();
- break;
- }
- } );
-
- // Remove when the input field loses focus.
- input.focusedProperty().addListener(
- ( focused, oldFocus, newFocus ) -> {
- if( !newFocus ) {
- getStatusBar().setGraphic( null );
- }
- }
- );
- }
-
- /**
- * Watch for changes to external files. In particular, this awaits
- * modifications to any XSL files associated with XML files being edited.
- * When
- * an XSL file is modified (external to the application), the snitch's ears
- * perk up and the file is reloaded. This keeps the XSL transformation up to
- * date with what's on the file system.
- */
- private void initSnitch() {
- SNITCH.addObserver( this );
- }
-
- /**
- * Listen for {@link FileEditorTabPane} to receive open definition file
- * event.
- */
- private void initDefinitionListener() {
- getFileEditorPane().onOpenDefinitionFileProperty().addListener(
- ( final ObservableValue<? extends Path> file,
- final Path oldPath, final Path newPath ) -> {
- // Indirectly refresh the resolved map.
- resetProcessors();
-
- openDefinitions( newPath );
-
- // Will create new processors and therefore a new resolved map.
- renderActiveTab();
- }
- );
- }
-
- /**
- * When tabs are added, hook the various change listeners onto the new
- * tab sothat the preview pane refreshes as necessary.
- */
- private void initTabAddedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Make sure the text processor kicks off when new files are opened.
- final ObservableList<Tab> tabs = editorPane.getTabs();
-
- // Update the preview pane on tab changes.
- tabs.addListener(
- ( final Change<? extends Tab> change ) -> {
- while( change.next() ) {
- if( change.wasAdded() ) {
- // Multiple tabs can be added simultaneously.
- for( final Tab newTab : change.getAddedSubList() ) {
- final FileEditorTab tab = (FileEditorTab) newTab;
-
- initTextChangeListener( tab );
- initTabKeyEventListener( tab );
- initScrollEventListener( tab );
-// initSyntaxListener( tab );
- }
- }
- }
- }
- );
- }
-
- private void initScrollEventListener( final FileEditorTab tab ) {
- final var scrollPane = tab.getScrollPane();
- final var scrollBar = getPreviewPane().getVerticalScrollBar();
-
- // Before the drag handler can be attached, the scroll bar for the
- // text editor pane must be visible.
- final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
- runLater( () -> {
- if( newShow ) {
- final var handler = new ScrollEventHandler( scrollPane, scrollBar );
- handler.enabledProperty().bind( tab.selectedProperty() );
- }
- } );
-
- Val.flatMap( scrollPane.sceneProperty(), Scene::windowProperty )
- .flatMap( Window::showingProperty )
- .addListener( listener );
- }
-
- /**
- * Listen for new tab selection events.
- */
- private void initTabChangedListener() {
- final FileEditorTabPane editorPane = getFileEditorPane();
-
- // Update the preview pane changing tabs.
- editorPane.addTabSelectionListener(
- ( tabPane, oldTab, newTab ) -> {
- if( newTab == null ) {
- getPreviewPane().clear();
- }
-
- // If there was no old tab, then this is a first time load, which
- // can be ignored.
- if( oldTab != null ) {
- if( newTab != null ) {
- final FileEditorTab tab = (FileEditorTab) newTab;
- updateVariableNameInjector( tab );
- process( tab );
- }
- }
- }
- );
- }
-
- /**
- * Reloads the preferences from the previous session.
- */
- private void initPreferences() {
- initDefinitionPane();
- getFileEditorPane().initPreferences();
- }
-
- private void initVariableNameInjector() {
- updateVariableNameInjector( getActiveFileEditorTab() );
- }
-
- /**
- * Ensure that the keyboard events are received when a new tab is added
- * to the user interface.
- *
- * @param tab The tab editor that can trigger keyboard events.
- */
- private void initTabKeyEventListener( final FileEditorTab tab ) {
- tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler );
- }
-
- private void initTextChangeListener( final FileEditorTab tab ) {
- tab.addTextChangeListener(
- ( editor, oldValue, newValue ) -> {
- process( tab );
- scrollToParagraph( getCurrentParagraphIndex() );
- }
- );
- }
-
- private int getCurrentParagraphIndex() {
- return getActiveEditorPane().getCurrentParagraphIndex();
- }
-
- private void scrollToParagraph( final int id ) {
- scrollToParagraph( id, false );
- }
-
- /**
- * @param id The paragraph to scroll to, will be approximated if it doesn't
- * exist.
- * @param force {@code true} means to force scrolling immediately, which
- * should only be attempted when it is known that the document
- * has been fully rendered. Otherwise the internal map of ID
- * attributes will be incomplete and scrolling will flounder.
- */
- private void scrollToParagraph( final int id, final boolean force ) {
- synchronized( mMutex ) {
- final var previewPane = getPreviewPane();
- final var scrollPane = previewPane.getScrollPane();
- final int approxId = getActiveEditorPane().approximateParagraphId( id );
-
- if( force ) {
- previewPane.scrollTo( approxId );
- }
- else {
- previewPane.tryScrollTo( approxId );
- }
-
- scrollPane.repaint();
- }
- }
-
- private void updateVariableNameInjector( final FileEditorTab tab ) {
- getVariableNameInjector().addListener( tab );
- }
-
- /**
- * Called whenever the preview pane becomes out of sync with the file editor
- * tab. This can be called when the text changes, the caret paragraph
- * changes, or the file tab changes.
- *
- * @param tab The file editor tab that has been changed in some fashion.
- */
- private void process( final FileEditorTab tab ) {
- if( tab == null ) {
- return;
- }
-
- getPreviewPane().setPath( tab.getPath() );
-
- final Processor<String> processor = getProcessors().computeIfAbsent(
- tab, p -> createProcessors( tab )
- );
-
- try {
- processChain( processor, tab.getEditorText() );
- } catch( final Exception ex ) {
- error( ex );
- }
- }
-
- /**
- * Executes the processing chain, operating on the given string.
- *
- * @param handler The first processor in the chain to call.
- * @param text The initial value of the text to process.
- * @return The final value of the text that was processed by the chain.
- */
- private String processChain( Processor<String> handler, String text ) {
- while( handler != null && text != null ) {
- text = handler.process( text );
- handler = handler.next();
- }
-
- return text;
- }
-
- private void renderActiveTab() {
- process( getActiveFileEditorTab() );
- }
-
- /**
- * Called when a definition source is opened.
- *
- * @param path Path to the definition source that was opened.
- */
- private void openDefinitions( final Path path ) {
- try {
- final DefinitionSource ds = createDefinitionSource( path );
- setDefinitionSource( ds );
- getUserPreferences().definitionPathProperty().setValue( path.toFile() );
- getUserPreferences().save();
-
- final Tooltip tooltipPath = new Tooltip( path.toString() );
- tooltipPath.setShowDelay( Duration.millis( 200 ) );
-
- final DefinitionPane pane = getDefinitionPane();
- pane.update( ds );
- pane.addTreeChangeHandler( mTreeHandler );
- pane.addKeyEventHandler( mDefinitionKeyHandler );
- pane.filenameProperty().setValue( path.getFileName().toString() );
- pane.setTooltip( tooltipPath );
-
- interpolateResolvedMap();
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private void exportDefinitions( final Path path ) {
- try {
- final DefinitionPane pane = getDefinitionPane();
- final TreeItem<String> root = pane.getTreeView().getRoot();
- final TreeItem<String> problemChild = pane.isTreeWellFormed();
-
- if( problemChild == null ) {
- getDefinitionSource().getTreeAdapter().export( root, path );
- getNotifier().clear();
- }
- else {
- final String msg = get(
- "yaml.error.tree.form", problemChild.getValue() );
- getNotifier().notify( msg );
- }
- } catch( final Exception e ) {
- error( e );
- }
- }
-
- private void interpolateResolvedMap() {
- final Map<String, String> treeMap = getDefinitionPane().toMap();
- final Map<String, String> map = new HashMap<>( treeMap );
- MapInterpolator.interpolate( map );
-
- getResolvedMap().clear();
- getResolvedMap().putAll( map );
- }
-
- private void initDefinitionPane() {
- openDefinitions( getDefinitionPath() );
- }
-
- /**
- * Called when an exception occurs that warrants the user's attention.
- *
- * @param e The exception with a message that the user should know about.
- */
- private void error( final Exception e ) {
- getNotifier().notify( e );
- }
-
- //---- File actions -------------------------------------------------------
-
- /**
- * Called when an {@link Observable} instance has changed. This is called
- * by both the {@link Snitch} service and the notify service. The @link
- * Snitch} service can be called for different file types, including
- * {@link DefinitionSource} instances.
- *
- * @param observable The observed instance.
- * @param value The noteworthy item.
- */
- @Override
- public void update( final Observable observable, final Object value ) {
- if( value != null ) {
- if( observable instanceof Snitch && value instanceof Path ) {
- updateSelectedTab();
- }
- else if( observable instanceof Notifier && value instanceof String ) {
- updateStatusBar( (String) value );
- }
- }
- }
-
- /**
- * Updates the status bar to show the given message.
- *
- * @param s The message to show in the status bar.
- */
- private void updateStatusBar( final String s ) {
- runLater(
- () -> {
- final int index = s.indexOf( '\n' );
- final String message = s.substring(
- 0, index > 0 ? index : s.length() );
-
- getStatusBar().setText( message );
- }
- );
- }
-
- /**
- * Called when a file has been modified.
- */
- private void updateSelectedTab() {
- runLater(
- () -> {
- // Brute-force XSLT file reload by re-instantiating all processors.
- resetProcessors();
- renderActiveTab();
- }
- );
- }
-
- /**
- * After resetting the processors, they will refresh anew to be up-to-date
- * with the files (text and definition) currently loaded into the editor.
- */
- private void resetProcessors() {
- getProcessors().clear();
- }
-
- //---- File actions -------------------------------------------------------
-
- private void fileNew() {
- getFileEditorPane().newEditor();
- }
-
- private void fileOpen() {
- getFileEditorPane().openFileDialog();
- }
-
- private void fileClose() {
- getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
- }
-
- /**
- * TODO: Upon closing, first remove the tab change listeners. (There's no
- * need to re-render each tab when all are being closed.)
- */
- private void fileCloseAll() {
- getFileEditorPane().closeAllEditors();
- }
-
- private void fileSave() {
- getFileEditorPane().saveEditor( getActiveFileEditorTab() );
- }
-
- private void fileSaveAs() {
- final FileEditorTab editor = getActiveFileEditorTab();
- getFileEditorPane().saveEditorAs( editor );
- getProcessors().remove( editor );
-
- try {
- process( editor );
- } catch( final Exception ex ) {
- getNotifier().notify( ex );
- }
- }
-
- private void fileSaveAll() {
- getFileEditorPane().saveAllEditors();
- }
-
- private void fileExit() {
- final Window window = getWindow();
- fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
- }
-
- //---- Edit actions -------------------------------------------------------
-
- /**
- * Transform the Markdown into HTML then copy that HTML into the copy
- * buffer.
- */
- private void copyHtml() {
- final var markdown = getActiveEditorPane().getText();
- final var processors = createProcessorFactory().createProcessors(
- getActiveFileEditorTab()
- );
-
- final var chain = processors.remove( HtmlPreviewProcessor.class );
-
- final String html = processChain( chain, markdown );
-
- final Clipboard clipboard = Clipboard.getSystemClipboard();
- final ClipboardContent content = new ClipboardContent();
- content.putString( html );
- clipboard.setContent( content );
- }
-
- /**
- * Used to find text in the active file editor window.
- */
- private void editFind() {
- final TextField input = getFindTextField();
- getStatusBar().setGraphic( input );
- input.requestFocus();
- }
-
- public void editFindNext() {
- getActiveFileEditorTab().searchNext( getFindTextField().getText() );
- }
-
- public void editPreferences() {
- getUserPreferences().show();
- }
-
- //---- Insert actions -----------------------------------------------------
-
- /**
- * Delegates to the active editor to handle wrapping the current text
- * selection with leading and trailing strings.
- *
- * @param leading The string to put before the selection.
- * @param trailing The string to put after the selection.
- */
- private void insertMarkdown(
- final String leading, final String trailing ) {
- getActiveEditorPane().surroundSelection( leading, trailing );
- }
-
- private void insertMarkdown(
- final String leading, final String trailing, final String hint ) {
- getActiveEditorPane().surroundSelection( leading, trailing, hint );
- }
-
- //---- Help actions -------------------------------------------------------
-
- private void helpAbout() {
- final Alert alert = new Alert( AlertType.INFORMATION );
- alert.setTitle( get( "Dialog.about.title" ) );
- alert.setHeaderText( get( "Dialog.about.header" ) );
- alert.setContentText( get( "Dialog.about.content" ) );
- alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
- alert.initOwner( getWindow() );
-
- alert.showAndWait();
- }
-
- //---- Member creators ----------------------------------------------------
-
- /**
- * Factory to create processors that are suited to different file types.
- *
- * @param tab The tab that is subjected to processing.
- * @return A processor suited to the file type specified by the tab's path.
- */
- private Processor<String> createProcessors( final FileEditorTab tab ) {
- return createProcessorFactory().createProcessors( tab );
- }
-
- private ProcessorFactory createProcessorFactory() {
- return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
- }
-
- private HTMLPreviewPane createHTMLPreviewPane() {
- return new HTMLPreviewPane();
- }
-
- private DefinitionSource createDefaultDefinitionSource() {
- return new YamlDefinitionSource( getDefinitionPath() );
- }
-
- private DefinitionSource createDefinitionSource( final Path path ) {
- try {
- return createDefinitionFactory().createDefinitionSource( path );
- } catch( final Exception ex ) {
- error( ex );
- return createDefaultDefinitionSource();
- }
- }
-
- private TextField createFindTextField() {
- return new TextField();
- }
-
- private DefinitionFactory createDefinitionFactory() {
- return new DefinitionFactory();
- }
-
- private StatusBar createStatusBar() {
- return new StatusBar();
- }
-
- private Scene createScene() {
- final SplitPane splitPane = new SplitPane(
- getDefinitionPane().getNode(),
- getFileEditorPane().getNode(),
- getPreviewPane().getNode() );
-
- splitPane.setDividerPositions(
- getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
- getFloat( K_PANE_SPLIT_EDITOR, .60f ),
- getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
-
- getDefinitionPane().prefHeightProperty()
- .bind( splitPane.heightProperty() );
-
- final BorderPane borderPane = new BorderPane();
- borderPane.setPrefSize( 1280, 800 );
- borderPane.setTop( createMenuBar() );
- borderPane.setBottom( getStatusBar() );
- borderPane.setCenter( splitPane );
-
- final VBox statusBar = new VBox();
- statusBar.setAlignment( Pos.BASELINE_CENTER );
- statusBar.getChildren().add( getLineNumberText() );
- getStatusBar().getRightItems().add( statusBar );
-
- // Force preview pane refresh on Windows.
- if( SystemUtils.IS_OS_WINDOWS ) {
- splitPane.getDividers().get( 1 ).positionProperty().addListener(
- ( l, oValue, nValue ) -> runLater(
- () -> getPreviewPane().getScrollPane().repaint()
- )
- );
- }
-
- return new Scene( borderPane );
- }
-
- private Text createLineNumberText() {
- return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
- }
-
- private Node createMenuBar() {
- final BooleanBinding activeFileEditorIsNull =
- getFileEditorPane().activeFileEditorProperty().isNull();
-
- // File actions
- final Action fileNewAction = new ActionBuilder()
- .setText( "Main.menu.file.new" )
- .setAccelerator( "Shortcut+N" )
- .setIcon( FILE_ALT )
- .setAction( e -> fileNew() )
- .build();
- final Action fileOpenAction = new ActionBuilder()
- .setText( "Main.menu.file.open" )
- .setAccelerator( "Shortcut+O" )
- .setIcon( FOLDER_OPEN_ALT )
- .setAction( e -> fileOpen() )
- .build();
- final Action fileCloseAction = new ActionBuilder()
- .setText( "Main.menu.file.close" )
- .setAccelerator( "Shortcut+W" )
- .setAction( e -> fileClose() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action fileCloseAllAction = new ActionBuilder()
- .setText( "Main.menu.file.close_all" )
- .setAction( e -> fileCloseAll() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action fileSaveAction = new ActionBuilder()
- .setText( "Main.menu.file.save" )
- .setAccelerator( "Shortcut+S" )
- .setIcon( FLOPPY_ALT )
- .setAction( e -> fileSave() )
- .setDisable( createActiveBooleanProperty(
- FileEditorTab::modifiedProperty ).not() )
- .build();
- final Action fileSaveAsAction = new ActionBuilder()
- .setText( "Main.menu.file.save_as" )
- .setAction( e -> fileSaveAs() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action fileSaveAllAction = new ActionBuilder()
- .setText( "Main.menu.file.save_all" )
- .setAccelerator( "Shortcut+Shift+S" )
- .setAction( e -> fileSaveAll() )
- .setDisable( Bindings.not(
- getFileEditorPane().anyFileEditorModifiedProperty() ) )
- .build();
- final Action fileExitAction = new ActionBuilder()
- .setText( "Main.menu.file.exit" )
- .setAction( e -> fileExit() )
- .build();
-
- // Edit actions
- final Action editCopyHtmlAction = new ActionBuilder()
- .setText( Messages.get( "Main.menu.edit.copy.html" ) )
- .setIcon( HTML5 )
- .setAction( e -> copyHtml() )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- final Action editUndoAction = new ActionBuilder()
- .setText( "Main.menu.edit.undo" )
- .setAccelerator( "Shortcut+Z" )
- .setIcon( UNDO )
- .setAction( e -> getActiveEditorPane().undo() )
- .setDisable( createActiveBooleanProperty(
- FileEditorTab::canUndoProperty ).not() )
- .build();
- final Action editRedoAction = new ActionBuilder()
- .setText( "Main.menu.edit.redo" )
- .setAccelerator( "Shortcut+Y" )
- .setIcon( REPEAT )
- .setAction( e -> getActiveEditorPane().redo() )
- .setDisable( createActiveBooleanProperty(
- FileEditorTab::canRedoProperty ).not() )
- .build();
-
- final Action editCutAction = new ActionBuilder()
- .setText( Messages.get( "Main.menu.edit.cut" ) )
- .setAccelerator( "Shortcut+X" )
- .setIcon( CUT )
- .setAction( e -> getActiveEditorPane().cut() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editCopyAction = new ActionBuilder()
- .setText( Messages.get( "Main.menu.edit.copy" ) )
- .setAccelerator( "Shortcut+C" )
- .setIcon( COPY )
- .setAction( e -> getActiveEditorPane().copy() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editPasteAction = new ActionBuilder()
- .setText( Messages.get( "Main.menu.edit.paste" ) )
- .setAccelerator( "Shortcut+V" )
- .setIcon( PASTE )
- .setAction( e -> getActiveEditorPane().paste() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editSelectAllAction = new ActionBuilder()
- .setText( Messages.get( "Main.menu.edit.selectAll" ) )
- .setAccelerator( "Shortcut+A" )
- .setAction( e -> getActiveEditorPane().selectAll() )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- final Action editFindAction = new ActionBuilder()
- .setText( "Main.menu.edit.find" )
- .setAccelerator( "Ctrl+F" )
- .setIcon( SEARCH )
- .setAction( e -> editFind() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editFindNextAction = new ActionBuilder()
- .setText( "Main.menu.edit.find.next" )
- .setAccelerator( "F3" )
- .setIcon( null )
- .setAction( e -> editFindNext() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action editPreferencesAction = new ActionBuilder()
- .setText( "Main.menu.edit.preferences" )
- .setAccelerator( "Ctrl+Alt+S" )
- .setAction( e -> editPreferences() )
- .build();
-
- // Insert actions
- final Action insertBoldAction = new ActionBuilder()
- .setText( "Main.menu.insert.bold" )
- .setAccelerator( "Shortcut+B" )
- .setIcon( BOLD )
- .setAction( e -> insertMarkdown( "**", "**" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertItalicAction = new ActionBuilder()
- .setText( "Main.menu.insert.italic" )
- .setAccelerator( "Shortcut+I" )
- .setIcon( ITALIC )
- .setAction( e -> insertMarkdown( "*", "*" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertSuperscriptAction = new ActionBuilder()
- .setText( "Main.menu.insert.superscript" )
- .setAccelerator( "Shortcut+[" )
- .setIcon( SUPERSCRIPT )
- .setAction( e -> insertMarkdown( "^", "^" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertSubscriptAction = new ActionBuilder()
- .setText( "Main.menu.insert.subscript" )
- .setAccelerator( "Shortcut+]" )
- .setIcon( SUBSCRIPT )
- .setAction( e -> insertMarkdown( "~", "~" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertStrikethroughAction = new ActionBuilder()
- .setText( "Main.menu.insert.strikethrough" )
- .setAccelerator( "Shortcut+T" )
- .setIcon( STRIKETHROUGH )
- .setAction( e -> insertMarkdown( "~~", "~~" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertBlockquoteAction = new ActionBuilder()
- .setText( "Main.menu.insert.blockquote" )
- .setAccelerator( "Ctrl+Q" )
- .setIcon( QUOTE_LEFT )
- .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertCodeAction = new ActionBuilder()
- .setText( "Main.menu.insert.code" )
- .setAccelerator( "Shortcut+K" )
- .setIcon( CODE )
- .setAction( e -> insertMarkdown( "`", "`" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertFencedCodeBlockAction = new ActionBuilder()
- .setText( "Main.menu.insert.fenced_code_block" )
- .setAccelerator( "Shortcut+Shift+K" )
- .setIcon( FILE_CODE_ALT )
- .setAction( e -> insertMarkdown(
- "\n\n```\n",
- "\n```\n\n",
- get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertLinkAction = new ActionBuilder()
- .setText( "Main.menu.insert.link" )
- .setAccelerator( "Shortcut+L" )
- .setIcon( LINK )
- .setAction( e -> getActiveEditorPane().insertLink() )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertImageAction = new ActionBuilder()
- .setText( "Main.menu.insert.image" )
- .setAccelerator( "Shortcut+G" )
- .setIcon( PICTURE_ALT )
- .setAction( e -> getActiveEditorPane().insertImage() )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- // Number of header actions (H1 ... H3)
- final int HEADERS = 3;
- final Action[] headers = new Action[ HEADERS ];
-
- for( int i = 1; i <= HEADERS; i++ ) {
- final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
- final String markup = String.format( "%n%n%s ", hashes );
- final String text = "Main.menu.insert.header." + i;
- final String accelerator = "Shortcut+" + i;
- final String prompt = text + ".prompt";
-
- headers[ i - 1 ] = new ActionBuilder()
- .setText( text )
- .setAccelerator( accelerator )
- .setIcon( HEADER )
- .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- }
-
- final Action insertUnorderedListAction = new ActionBuilder()
- .setText( "Main.menu.insert.unordered_list" )
- .setAccelerator( "Shortcut+U" )
- .setIcon( LIST_UL )
- .setAction( e -> insertMarkdown( "\n\n* ", "" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertOrderedListAction = new ActionBuilder()
- .setText( "Main.menu.insert.ordered_list" )
- .setAccelerator( "Shortcut+Shift+O" )
- .setIcon( LIST_OL )
- .setAction( e -> insertMarkdown(
- "\n\n1. ", "" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
- final Action insertHorizontalRuleAction = new ActionBuilder()
- .setText( "Main.menu.insert.horizontal_rule" )
- .setAccelerator( "Shortcut+H" )
- .setAction( e -> insertMarkdown(
- "\n\n---\n\n", "" ) )
- .setDisable( activeFileEditorIsNull )
- .build();
-
- // Help actions
- final Action helpAboutAction = new ActionBuilder()
- .setText( "Main.menu.help.about" )
- .setAction( e -> helpAbout() )
- .build();
-
- //---- MenuBar ----
- final Menu fileMenu = ActionUtils.createMenu(
- get( "Main.menu.file" ),
- fileNewAction,
- fileOpenAction,
- null,
- fileCloseAction,
- fileCloseAllAction,
- null,
- fileSaveAction,
- fileSaveAsAction,
- fileSaveAllAction,
- null,
- fileExitAction );
-
- final Menu editMenu = ActionUtils.createMenu(
- get( "Main.menu.edit" ),
- editCopyHtmlAction,
- null,
- editUndoAction,
- editRedoAction,
- null,
- editCutAction,
- editCopyAction,
- editPasteAction,
- null,
- editFindAction,
- editFindNextAction,
- null,
- editPreferencesAction );
-
- final Menu insertMenu = ActionUtils.createMenu(
- get( "Main.menu.insert" ),
- insertBoldAction,
- insertItalicAction,
- insertSuperscriptAction,
- insertSubscriptAction,
- insertStrikethroughAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headers[ 0 ],
- headers[ 1 ],
- headers[ 2 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction,
- insertHorizontalRuleAction
- );
-
- final Menu helpMenu = ActionUtils.createMenu(
- get( "Main.menu.help" ),
- helpAboutAction );
-
- final MenuBar menuBar = new MenuBar(
- fileMenu,
- editMenu,
- insertMenu,
- helpMenu );
-
- //---- ToolBar ----
- final ToolBar toolBar = ActionUtils.createToolBar(
- fileNewAction,
- fileOpenAction,
- fileSaveAction,
- null,
- editUndoAction,
- editRedoAction,
- editCutAction,
- editCopyAction,
- editPasteAction,
- null,
- insertBoldAction,
- insertItalicAction,
- insertSuperscriptAction,
- insertSubscriptAction,
- insertBlockquoteAction,
- insertCodeAction,
- insertFencedCodeBlockAction,
- null,
- insertLinkAction,
- insertImageAction,
- null,
- headers[ 0 ],
- null,
- insertUnorderedListAction,
- insertOrderedListAction );
-
- return new VBox( menuBar, toolBar );
- }
-
- /**
- * Creates a boolean property that is bound to another boolean value of the
- * active editor.
- */
- private BooleanProperty createActiveBooleanProperty(
- final Function<FileEditorTab, ObservableBooleanValue> func ) {
-
- final BooleanProperty b = new SimpleBooleanProperty();
- final FileEditorTab tab = getActiveFileEditorTab();
-
- if( tab != null ) {
- b.bind( func.apply( tab ) );
- }
-
- getFileEditorPane().activeFileEditorProperty().addListener(
- ( observable, oldFileEditor, newFileEditor ) -> {
- b.unbind();
-
- if( newFileEditor == null ) {
- b.set( false );
- }
- else {
- b.bind( func.apply( newFileEditor ) );
- }
- }
- );
-
- return b;
- }
-
- //---- Convenience accessors ----------------------------------------------
-
- private Preferences getPreferences() {
- return OPTIONS.getState();
- }
-
- private float getFloat( final String key, final float defaultValue ) {
- return getPreferences().getFloat( key, defaultValue );
- }
-
- public Window getWindow() {
- return getScene().getWindow();
- }
-
- private MarkdownEditorPane getActiveEditorPane() {
- return getActiveFileEditorTab().getEditorPane();
- }
-
- private FileEditorTab getActiveFileEditorTab() {
- return getFileEditorPane().getActiveFileEditor();
- }
-
- //---- Member accessors ---------------------------------------------------
-
- protected Scene getScene() {
- return mScene;
- }
-
- private Map<FileEditorTab, Processor<String>> getProcessors() {
- return mProcessors;
- }
-
- private FileEditorTabPane getFileEditorPane() {
- return mFileEditorPane;
- }
-
- private HTMLPreviewPane getPreviewPane() {
- return mPreviewPane;
- }
-
- private void setDefinitionSource(
- final DefinitionSource definitionSource ) {
- assert definitionSource != null;
- mDefinitionSource = definitionSource;
- }
-
- private DefinitionSource getDefinitionSource() {
- return mDefinitionSource;
- }
-
- private DefinitionPane getDefinitionPane() {
- return mDefinitionPane;
- }
-
- private Text getLineNumberText() {
- return mLineNumberText;
- }
-
- private StatusBar getStatusBar() {
- return mStatusBar;
- }
-
- private TextField getFindTextField() {
- return mFindTextField;
- }
-
- private VariableNameInjector getVariableNameInjector() {
- return mVariableNameInjector;
- }
-
- /**
- * Returns the variable map of interpolated definitions.
- *
- * @return A map to help dereference variables.
- */
- private Map<String, String> getResolvedMap() {
- return mResolvedMap;
- }
-
- private Notifier getNotifier() {
- return NOTIFIER;
- }
-
- //---- Persistence accessors ----------------------------------------------
-
- private UserPreferences getUserPreferences() {
- return OPTIONS.getUserPreferences();
- }
-
- private Path getDefinitionPath() {
- return getUserPreferences().getDefinitionPath();
+import com.scrivenvar.spelling.api.SpellChecker;
+import com.scrivenvar.spelling.impl.PermissiveSpeller;
+import com.scrivenvar.spelling.impl.SymSpellSpeller;
+import com.scrivenvar.util.Action;
+import com.scrivenvar.util.ActionBuilder;
+import com.scrivenvar.util.ActionUtils;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.stage.Window;
+import javafx.stage.WindowEvent;
+import javafx.util.Duration;
+import org.apache.commons.lang3.SystemUtils;
+import org.controlsfx.control.StatusBar;
+import org.fxmisc.richtext.StyleClassedTextArea;
+import org.fxmisc.richtext.model.StyleSpansBuilder;
+import org.reactfx.value.Val;
+import org.xhtmlrenderer.util.XRLog;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.prefs.Preferences;
+import java.util.stream.Collectors;
+
+import static com.scrivenvar.Constants.*;
+import static com.scrivenvar.Messages.get;
+import static com.scrivenvar.util.StageState.*;
+import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static javafx.application.Platform.runLater;
+import static javafx.event.Event.fireEvent;
+import static javafx.scene.input.KeyCode.ENTER;
+import static javafx.scene.input.KeyCode.TAB;
+import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
+import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
+
+/**
+ * Main window containing a tab pane in the center for file editors.
+ */
+public class MainWindow implements Observer {
+ /**
+ * The {@code OPTIONS} variable must be declared before all other variables
+ * to prevent subsequent initializations from failing due to missing user
+ * preferences.
+ */
+ private final static Options OPTIONS = Services.load( Options.class );
+ private final static Snitch SNITCH = Services.load( Snitch.class );
+ private final static Notifier NOTIFIER = Services.load( Notifier.class );
+
+ private final Scene mScene;
+ private final StatusBar mStatusBar;
+ private final Text mLineNumberText;
+ private final TextField mFindTextField;
+ private final SpellChecker mSpellChecker;
+
+ private final Object mMutex = new Object();
+
+ /**
+ * Prevents re-instantiation of processing classes.
+ */
+ private final Map<FileEditorTab, Processor<String>> mProcessors =
+ new HashMap<>();
+
+ private final Map<String, String> mResolvedMap =
+ new HashMap<>( DEFAULT_MAP_SIZE );
+
+ /**
+ * Called when the definition data is changed.
+ */
+ private final EventHandler<TreeItem.TreeModificationEvent<Event>>
+ mTreeHandler = event -> {
+ exportDefinitions( getDefinitionPath() );
+ interpolateResolvedMap();
+ renderActiveTab();
+ };
+
+ /**
+ * Called to switch to the definition pane when the user presses the TAB key.
+ */
+ private final EventHandler<? super KeyEvent> mTabKeyHandler =
+ (EventHandler<KeyEvent>) event -> {
+ if( event.getCode() == TAB ) {
+ getDefinitionPane().requestFocus();
+ event.consume();
+ }
+ };
+
+ /**
+ * Called to inject the selected item when the user presses ENTER in the
+ * definition pane.
+ */
+ private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
+ event -> {
+ if( event.getCode() == ENTER ) {
+ getVariableNameInjector().injectSelectedItem();
+ }
+ };
+
+ private final ChangeListener<Integer> mCaretPositionListener =
+ ( observable, oldPosition, newPosition ) -> {
+ final FileEditorTab tab = getActiveFileEditorTab();
+ final EditorPane pane = tab.getEditorPane();
+ final StyleClassedTextArea editor = pane.getEditor();
+
+ getLineNumberText().setText(
+ get( STATUS_BAR_LINE,
+ editor.getCurrentParagraph() + 1,
+ editor.getParagraphs().size(),
+ editor.getCaretPosition()
+ )
+ );
+ };
+
+ private final ChangeListener<Integer> mCaretParagraphListener =
+ ( observable, oldIndex, newIndex ) ->
+ scrollToParagraph( newIndex, true );
+
+ private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
+ private final DefinitionPane mDefinitionPane = new DefinitionPane();
+ private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
+ private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
+ mCaretPositionListener,
+ mCaretParagraphListener );
+
+ /**
+ * Listens on the definition pane for double-click events.
+ */
+ private final VariableNameInjector mVariableNameInjector
+ = new VariableNameInjector( mDefinitionPane );
+
+ public MainWindow() {
+ mStatusBar = createStatusBar();
+ mLineNumberText = createLineNumberText();
+ mFindTextField = createFindTextField();
+ mScene = createScene();
+ mSpellChecker = createSpellChecker();
+
+ System.getProperties()
+ .setProperty( "xr.util-logging.loggingEnabled", "true" );
+ XRLog.setLoggingEnabled( true );
+
+ initLayout();
+ initFindInput();
+ initSnitch();
+ initDefinitionListener();
+ initTabAddedListener();
+ initTabChangedListener();
+ initPreferences();
+ initVariableNameInjector();
+
+ NOTIFIER.addObserver( this );
+ }
+
+ private void initLayout() {
+ final Scene appScene = getScene();
+
+ appScene.getStylesheets().add( STYLESHEET_SCENE );
+
+ // TODO: Apply an XML syntax highlighting for XML files.
+// appScene.getStylesheets().add( STYLESHEET_XML );
+ appScene.windowProperty().addListener(
+ ( observable, oldWindow, newWindow ) ->
+ newWindow.setOnCloseRequest(
+ e -> {
+ if( !getFileEditorPane().closeAllEditors() ) {
+ e.consume();
+ }
+ }
+ )
+ );
+ }
+
+ /**
+ * Initialize the find input text field to listen on F3, ENTER, and
+ * ESCAPE key presses.
+ */
+ private void initFindInput() {
+ final TextField input = getFindTextField();
+
+ input.setOnKeyPressed( ( KeyEvent event ) -> {
+ switch( event.getCode() ) {
+ case F3:
+ case ENTER:
+ editFindNext();
+ break;
+ case F:
+ if( !event.isControlDown() ) {
+ break;
+ }
+ case ESCAPE:
+ getStatusBar().setGraphic( null );
+ getActiveFileEditorTab().getEditorPane().requestFocus();
+ break;
+ }
+ } );
+
+ // Remove when the input field loses focus.
+ input.focusedProperty().addListener(
+ ( focused, oldFocus, newFocus ) -> {
+ if( !newFocus ) {
+ getStatusBar().setGraphic( null );
+ }
+ }
+ );
+ }
+
+ /**
+ * Watch for changes to external files. In particular, this awaits
+ * modifications to any XSL files associated with XML files being edited.
+ * When
+ * an XSL file is modified (external to the application), the snitch's ears
+ * perk up and the file is reloaded. This keeps the XSL transformation up to
+ * date with what's on the file system.
+ */
+ private void initSnitch() {
+ SNITCH.addObserver( this );
+ }
+
+ /**
+ * Listen for {@link FileEditorTabPane} to receive open definition file
+ * event.
+ */
+ private void initDefinitionListener() {
+ getFileEditorPane().onOpenDefinitionFileProperty().addListener(
+ ( final ObservableValue<? extends Path> file,
+ final Path oldPath, final Path newPath ) -> {
+ // Indirectly refresh the resolved map.
+ resetProcessors();
+
+ openDefinitions( newPath );
+
+ // Will create new processors and therefore a new resolved map.
+ renderActiveTab();
+ }
+ );
+ }
+
+ /**
+ * When tabs are added, hook the various change listeners onto the new
+ * tab sothat the preview pane refreshes as necessary.
+ */
+ private void initTabAddedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Make sure the text processor kicks off when new files are opened.
+ final ObservableList<Tab> tabs = editorPane.getTabs();
+
+ // Update the preview pane on tab changes.
+ tabs.addListener(
+ ( final Change<? extends Tab> change ) -> {
+ while( change.next() ) {
+ if( change.wasAdded() ) {
+ // Multiple tabs can be added simultaneously.
+ for( final Tab newTab : change.getAddedSubList() ) {
+ final FileEditorTab tab = (FileEditorTab) newTab;
+
+ initTextChangeListener( tab );
+ initTabKeyEventListener( tab );
+ initScrollEventListener( tab );
+ initSpellCheckListener( tab );
+// initSyntaxListener( tab );
+ }
+ }
+ }
+ }
+ );
+ }
+
+ private void initTextChangeListener( final FileEditorTab tab ) {
+ tab.addTextChangeListener(
+ ( editor, oldValue, newValue ) -> {
+ process( tab );
+ scrollToParagraph( getCurrentParagraphIndex() );
+ }
+ );
+ }
+
+ /**
+ * Ensure that the keyboard events are received when a new tab is added
+ * to the user interface.
+ *
+ * @param tab The tab editor that can trigger keyboard events.
+ */
+ private void initTabKeyEventListener( final FileEditorTab tab ) {
+ tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler );
+ }
+
+ private void initScrollEventListener( final FileEditorTab tab ) {
+ final var scrollPane = tab.getScrollPane();
+ final var scrollBar = getPreviewPane().getVerticalScrollBar();
+
+ // Before the drag handler can be attached, the scroll bar for the
+ // text editor pane must be visible.
+ final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
+ runLater( () -> {
+ if( newShow ) {
+ final var handler = new ScrollEventHandler( scrollPane, scrollBar );
+ handler.enabledProperty().bind( tab.selectedProperty() );
+ }
+ } );
+
+ Val.flatMap( scrollPane.sceneProperty(), Scene::windowProperty )
+ .flatMap( Window::showingProperty )
+ .addListener( listener );
+ }
+
+ /**
+ * Listen for changes to the any particular paragraph and perform a quick
+ * spell check upon it. The style classes in the editor will be changed to
+ * mark any spelling mistakes in the paragraph. The user may then interact
+ * with any misspelled word (i.e., any piece of text that is marked) to
+ * revise the spelling.
+ *
+ * @param tab The tab to spellcheck.
+ */
+ private void initSpellCheckListener( final FileEditorTab tab ) {
+ final var editor = tab.getEditorPane().getEditor();
+
+ // Use the plain text changes so that notifications of style changes
+ // are suppressed.
+ editor.plainTextChanges()
+ .filter( p -> !p.isIdentity() ).subscribe( change -> {
+
+ // Only perform a spell check on the current paragraph. The
+ // entire document is processed once, when opened.
+ final var offset = change.getPosition();
+ final var position = editor.offsetToPosition( offset, Forward );
+ final var paraId = position.getMajor();
+ final var paragraph = editor.getParagraph( paraId );
+ final var text = paragraph.getText();
+
+ editor.clearStyle( paraId );
+
+ final var builder = new StyleSpansBuilder<Collection<String>>();
+ final var count = new AtomicInteger( 0 );
+ final var runningIndex = new AtomicInteger( 0 );
+
+ getSpellChecker().proofread( text, ( prevIndex, currIndex ) -> {
+ builder.add( emptyList(), prevIndex - runningIndex.get() );
+ builder.add( singleton( "spelling" ), currIndex - prevIndex );
+ count.incrementAndGet();
+ runningIndex.set( currIndex );
+ } );
+
+ if( count.get() > 0 ) {
+ builder.add( emptyList(), text.length() - runningIndex.get() );
+
+ final var spans = builder.create();
+ editor.setStyleSpans( paraId, 0, spans );
+ }
+ } );
+ }
+
+ /**
+ * Listen for new tab selection events.
+ */
+ private void initTabChangedListener() {
+ final FileEditorTabPane editorPane = getFileEditorPane();
+
+ // Update the preview pane changing tabs.
+ editorPane.addTabSelectionListener(
+ ( tabPane, oldTab, newTab ) -> {
+ if( newTab == null ) {
+ getPreviewPane().clear();
+ }
+
+ // If there was no old tab, then this is a first time load, which
+ // can be ignored.
+ if( oldTab != null ) {
+ if( newTab != null ) {
+ final FileEditorTab tab = (FileEditorTab) newTab;
+ updateVariableNameInjector( tab );
+ process( tab );
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Reloads the preferences from the previous session.
+ */
+ private void initPreferences() {
+ initDefinitionPane();
+ getFileEditorPane().initPreferences();
+ }
+
+ private void initVariableNameInjector() {
+ updateVariableNameInjector( getActiveFileEditorTab() );
+ }
+
+ private int getCurrentParagraphIndex() {
+ return getActiveEditorPane().getCurrentParagraphIndex();
+ }
+
+ private void scrollToParagraph( final int id ) {
+ scrollToParagraph( id, false );
+ }
+
+ /**
+ * @param id The paragraph to scroll to, will be approximated if it doesn't
+ * exist.
+ * @param force {@code true} means to force scrolling immediately, which
+ * should only be attempted when it is known that the document
+ * has been fully rendered. Otherwise the internal map of ID
+ * attributes will be incomplete and scrolling will flounder.
+ */
+ private void scrollToParagraph( final int id, final boolean force ) {
+ synchronized( mMutex ) {
+ final var previewPane = getPreviewPane();
+ final var scrollPane = previewPane.getScrollPane();
+ final int approxId = getActiveEditorPane().approximateParagraphId( id );
+
+ if( force ) {
+ previewPane.scrollTo( approxId );
+ }
+ else {
+ previewPane.tryScrollTo( approxId );
+ }
+
+ scrollPane.repaint();
+ }
+ }
+
+ private void updateVariableNameInjector( final FileEditorTab tab ) {
+ getVariableNameInjector().addListener( tab );
+ }
+
+ /**
+ * Called whenever the preview pane becomes out of sync with the file editor
+ * tab. This can be called when the text changes, the caret paragraph
+ * changes, or the file tab changes.
+ *
+ * @param tab The file editor tab that has been changed in some fashion.
+ */
+ private void process( final FileEditorTab tab ) {
+ if( tab == null ) {
+ return;
+ }
+
+ getPreviewPane().setPath( tab.getPath() );
+
+ final Processor<String> processor = getProcessors().computeIfAbsent(
+ tab, p -> createProcessors( tab )
+ );
+
+ try {
+ processChain( processor, tab.getEditorText() );
+ } catch( final Exception ex ) {
+ error( ex );
+ }
+ }
+
+ /**
+ * Executes the processing chain, operating on the given string.
+ *
+ * @param handler The first processor in the chain to call.
+ * @param text The initial value of the text to process.
+ * @return The final value of the text that was processed by the chain.
+ */
+ private String processChain( Processor<String> handler, String text ) {
+ while( handler != null && text != null ) {
+ text = handler.process( text );
+ handler = handler.next();
+ }
+
+ return text;
+ }
+
+ private void renderActiveTab() {
+ process( getActiveFileEditorTab() );
+ }
+
+ /**
+ * Called when a definition source is opened.
+ *
+ * @param path Path to the definition source that was opened.
+ */
+ private void openDefinitions( final Path path ) {
+ try {
+ final DefinitionSource ds = createDefinitionSource( path );
+ setDefinitionSource( ds );
+ getUserPreferences().definitionPathProperty().setValue( path.toFile() );
+ getUserPreferences().save();
+
+ final Tooltip tooltipPath = new Tooltip( path.toString() );
+ tooltipPath.setShowDelay( Duration.millis( 200 ) );
+
+ final DefinitionPane pane = getDefinitionPane();
+ pane.update( ds );
+ pane.addTreeChangeHandler( mTreeHandler );
+ pane.addKeyEventHandler( mDefinitionKeyHandler );
+ pane.filenameProperty().setValue( path.getFileName().toString() );
+ pane.setTooltip( tooltipPath );
+
+ interpolateResolvedMap();
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private void exportDefinitions( final Path path ) {
+ try {
+ final DefinitionPane pane = getDefinitionPane();
+ final TreeItem<String> root = pane.getTreeView().getRoot();
+ final TreeItem<String> problemChild = pane.isTreeWellFormed();
+
+ if( problemChild == null ) {
+ getDefinitionSource().getTreeAdapter().export( root, path );
+ getNotifier().clear();
+ }
+ else {
+ final String msg = get(
+ "yaml.error.tree.form", problemChild.getValue() );
+ getNotifier().notify( msg );
+ }
+ } catch( final Exception e ) {
+ error( e );
+ }
+ }
+
+ private void interpolateResolvedMap() {
+ final Map<String, String> treeMap = getDefinitionPane().toMap();
+ final Map<String, String> map = new HashMap<>( treeMap );
+ MapInterpolator.interpolate( map );
+
+ getResolvedMap().clear();
+ getResolvedMap().putAll( map );
+ }
+
+ private void initDefinitionPane() {
+ openDefinitions( getDefinitionPath() );
+ }
+
+ /**
+ * Called when an exception occurs that warrants the user's attention.
+ *
+ * @param e The exception with a message that the user should know about.
+ */
+ private void error( final Exception e ) {
+ getNotifier().notify( e );
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ /**
+ * Called when an {@link Observable} instance has changed. This is called
+ * by both the {@link Snitch} service and the notify service. The @link
+ * Snitch} service can be called for different file types, including
+ * {@link DefinitionSource} instances.
+ *
+ * @param observable The observed instance.
+ * @param value The noteworthy item.
+ */
+ @Override
+ public void update( final Observable observable, final Object value ) {
+ if( value != null ) {
+ if( observable instanceof Snitch && value instanceof Path ) {
+ updateSelectedTab();
+ }
+ else if( observable instanceof Notifier && value instanceof String ) {
+ updateStatusBar( (String) value );
+ }
+ }
+ }
+
+ /**
+ * Updates the status bar to show the given message.
+ *
+ * @param s The message to show in the status bar.
+ */
+ private void updateStatusBar( final String s ) {
+ runLater(
+ () -> {
+ final int index = s.indexOf( '\n' );
+ final String message = s.substring(
+ 0, index > 0 ? index : s.length() );
+
+ getStatusBar().setText( message );
+ }
+ );
+ }
+
+ /**
+ * Called when a file has been modified.
+ */
+ private void updateSelectedTab() {
+ runLater(
+ () -> {
+ // Brute-force XSLT file reload by re-instantiating all processors.
+ resetProcessors();
+ renderActiveTab();
+ }
+ );
+ }
+
+ /**
+ * After resetting the processors, they will refresh anew to be up-to-date
+ * with the files (text and definition) currently loaded into the editor.
+ */
+ private void resetProcessors() {
+ getProcessors().clear();
+ }
+
+ //---- File actions -------------------------------------------------------
+
+ private void fileNew() {
+ getFileEditorPane().newEditor();
+ }
+
+ private void fileOpen() {
+ getFileEditorPane().openFileDialog();
+ }
+
+ private void fileClose() {
+ getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
+ }
+
+ /**
+ * TODO: Upon closing, first remove the tab change listeners. (There's no
+ * need to re-render each tab when all are being closed.)
+ */
+ private void fileCloseAll() {
+ getFileEditorPane().closeAllEditors();
+ }
+
+ private void fileSave() {
+ getFileEditorPane().saveEditor( getActiveFileEditorTab() );
+ }
+
+ private void fileSaveAs() {
+ final FileEditorTab editor = getActiveFileEditorTab();
+ getFileEditorPane().saveEditorAs( editor );
+ getProcessors().remove( editor );
+
+ try {
+ process( editor );
+ } catch( final Exception ex ) {
+ getNotifier().notify( ex );
+ }
+ }
+
+ private void fileSaveAll() {
+ getFileEditorPane().saveAllEditors();
+ }
+
+ private void fileExit() {
+ final Window window = getWindow();
+ fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
+ }
+
+ //---- Edit actions -------------------------------------------------------
+
+ /**
+ * Transform the Markdown into HTML then copy that HTML into the copy
+ * buffer.
+ */
+ private void copyHtml() {
+ final var markdown = getActiveEditorPane().getText();
+ final var processors = createProcessorFactory().createProcessors(
+ getActiveFileEditorTab()
+ );
+
+ final var chain = processors.remove( HtmlPreviewProcessor.class );
+
+ final String html = processChain( chain, markdown );
+
+ final Clipboard clipboard = Clipboard.getSystemClipboard();
+ final ClipboardContent content = new ClipboardContent();
+ content.putString( html );
+ clipboard.setContent( content );
+ }
+
+ /**
+ * Used to find text in the active file editor window.
+ */
+ private void editFind() {
+ final TextField input = getFindTextField();
+ getStatusBar().setGraphic( input );
+ input.requestFocus();
+ }
+
+ public void editFindNext() {
+ getActiveFileEditorTab().searchNext( getFindTextField().getText() );
+ }
+
+ public void editPreferences() {
+ getUserPreferences().show();
+ }
+
+ //---- Insert actions -----------------------------------------------------
+
+ /**
+ * Delegates to the active editor to handle wrapping the current text
+ * selection with leading and trailing strings.
+ *
+ * @param leading The string to put before the selection.
+ * @param trailing The string to put after the selection.
+ */
+ private void insertMarkdown(
+ final String leading, final String trailing ) {
+ getActiveEditorPane().surroundSelection( leading, trailing );
+ }
+
+ private void insertMarkdown(
+ final String leading, final String trailing, final String hint ) {
+ getActiveEditorPane().surroundSelection( leading, trailing, hint );
+ }
+
+ //---- Help actions -------------------------------------------------------
+
+ private void helpAbout() {
+ final Alert alert = new Alert( AlertType.INFORMATION );
+ alert.setTitle( get( "Dialog.about.title" ) );
+ alert.setHeaderText( get( "Dialog.about.header" ) );
+ alert.setContentText( get( "Dialog.about.content" ) );
+ alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
+ alert.initOwner( getWindow() );
+
+ alert.showAndWait();
+ }
+
+ //---- Member creators ----------------------------------------------------
+
+ private SpellChecker createSpellChecker() {
+ try {
+ final Collection<String> lexicon = readLexicon( "en.txt" );
+ return SymSpellSpeller.forLexicon( lexicon );
+ } catch( final Exception e ) {
+ getNotifier().notify( e );
+ return new PermissiveSpeller();
+ }
+ }
+
+ /**
+ * Factory to create processors that are suited to different file types.
+ *
+ * @param tab The tab that is subjected to processing.
+ * @return A processor suited to the file type specified by the tab's path.
+ */
+ private Processor<String> createProcessors( final FileEditorTab tab ) {
+ return createProcessorFactory().createProcessors( tab );
+ }
+
+ private ProcessorFactory createProcessorFactory() {
+ return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
+ }
+
+ private HTMLPreviewPane createHTMLPreviewPane() {
+ return new HTMLPreviewPane();
+ }
+
+ private DefinitionSource createDefaultDefinitionSource() {
+ return new YamlDefinitionSource( getDefinitionPath() );
+ }
+
+ private DefinitionSource createDefinitionSource( final Path path ) {
+ try {
+ return createDefinitionFactory().createDefinitionSource( path );
+ } catch( final Exception ex ) {
+ error( ex );
+ return createDefaultDefinitionSource();
+ }
+ }
+
+ private TextField createFindTextField() {
+ return new TextField();
+ }
+
+ private DefinitionFactory createDefinitionFactory() {
+ return new DefinitionFactory();
+ }
+
+ private StatusBar createStatusBar() {
+ return new StatusBar();
+ }
+
+ private Scene createScene() {
+ final SplitPane splitPane = new SplitPane(
+ getDefinitionPane().getNode(),
+ getFileEditorPane().getNode(),
+ getPreviewPane().getNode() );
+
+ splitPane.setDividerPositions(
+ getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
+ getFloat( K_PANE_SPLIT_EDITOR, .60f ),
+ getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
+
+ getDefinitionPane().prefHeightProperty()
+ .bind( splitPane.heightProperty() );
+
+ final BorderPane borderPane = new BorderPane();
+ borderPane.setPrefSize( 1280, 800 );
+ borderPane.setTop( createMenuBar() );
+ borderPane.setBottom( getStatusBar() );
+ borderPane.setCenter( splitPane );
+
+ final VBox statusBar = new VBox();
+ statusBar.setAlignment( Pos.BASELINE_CENTER );
+ statusBar.getChildren().add( getLineNumberText() );
+ getStatusBar().getRightItems().add( statusBar );
+
+ // Force preview pane refresh on Windows.
+ if( SystemUtils.IS_OS_WINDOWS ) {
+ splitPane.getDividers().get( 1 ).positionProperty().addListener(
+ ( l, oValue, nValue ) -> runLater(
+ () -> getPreviewPane().getScrollPane().repaint()
+ )
+ );
+ }
+
+ return new Scene( borderPane );
+ }
+
+ private Text createLineNumberText() {
+ return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
+ }
+
+ private Node createMenuBar() {
+ final BooleanBinding activeFileEditorIsNull =
+ getFileEditorPane().activeFileEditorProperty().isNull();
+
+ // File actions
+ final Action fileNewAction = new ActionBuilder()
+ .setText( "Main.menu.file.new" )
+ .setAccelerator( "Shortcut+N" )
+ .setIcon( FILE_ALT )
+ .setAction( e -> fileNew() )
+ .build();
+ final Action fileOpenAction = new ActionBuilder()
+ .setText( "Main.menu.file.open" )
+ .setAccelerator( "Shortcut+O" )
+ .setIcon( FOLDER_OPEN_ALT )
+ .setAction( e -> fileOpen() )
+ .build();
+ final Action fileCloseAction = new ActionBuilder()
+ .setText( "Main.menu.file.close" )
+ .setAccelerator( "Shortcut+W" )
+ .setAction( e -> fileClose() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action fileCloseAllAction = new ActionBuilder()
+ .setText( "Main.menu.file.close_all" )
+ .setAction( e -> fileCloseAll() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action fileSaveAction = new ActionBuilder()
+ .setText( "Main.menu.file.save" )
+ .setAccelerator( "Shortcut+S" )
+ .setIcon( FLOPPY_ALT )
+ .setAction( e -> fileSave() )
+ .setDisable( createActiveBooleanProperty(
+ FileEditorTab::modifiedProperty ).not() )
+ .build();
+ final Action fileSaveAsAction = new ActionBuilder()
+ .setText( "Main.menu.file.save_as" )
+ .setAction( e -> fileSaveAs() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action fileSaveAllAction = new ActionBuilder()
+ .setText( "Main.menu.file.save_all" )
+ .setAccelerator( "Shortcut+Shift+S" )
+ .setAction( e -> fileSaveAll() )
+ .setDisable( Bindings.not(
+ getFileEditorPane().anyFileEditorModifiedProperty() ) )
+ .build();
+ final Action fileExitAction = new ActionBuilder()
+ .setText( "Main.menu.file.exit" )
+ .setAction( e -> fileExit() )
+ .build();
+
+ // Edit actions
+ final Action editCopyHtmlAction = new ActionBuilder()
+ .setText( Messages.get( "Main.menu.edit.copy.html" ) )
+ .setIcon( HTML5 )
+ .setAction( e -> copyHtml() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ final Action editUndoAction = new ActionBuilder()
+ .setText( "Main.menu.edit.undo" )
+ .setAccelerator( "Shortcut+Z" )
+ .setIcon( UNDO )
+ .setAction( e -> getActiveEditorPane().undo() )
+ .setDisable( createActiveBooleanProperty(
+ FileEditorTab::canUndoProperty ).not() )
+ .build();
+ final Action editRedoAction = new ActionBuilder()
+ .setText( "Main.menu.edit.redo" )
+ .setAccelerator( "Shortcut+Y" )
+ .setIcon( REPEAT )
+ .setAction( e -> getActiveEditorPane().redo() )
+ .setDisable( createActiveBooleanProperty(
+ FileEditorTab::canRedoProperty ).not() )
+ .build();
+
+ final Action editCutAction = new ActionBuilder()
+ .setText( Messages.get( "Main.menu.edit.cut" ) )
+ .setAccelerator( "Shortcut+X" )
+ .setIcon( CUT )
+ .setAction( e -> getActiveEditorPane().cut() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editCopyAction = new ActionBuilder()
+ .setText( Messages.get( "Main.menu.edit.copy" ) )
+ .setAccelerator( "Shortcut+C" )
+ .setIcon( COPY )
+ .setAction( e -> getActiveEditorPane().copy() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editPasteAction = new ActionBuilder()
+ .setText( Messages.get( "Main.menu.edit.paste" ) )
+ .setAccelerator( "Shortcut+V" )
+ .setIcon( PASTE )
+ .setAction( e -> getActiveEditorPane().paste() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editSelectAllAction = new ActionBuilder()
+ .setText( Messages.get( "Main.menu.edit.selectAll" ) )
+ .setAccelerator( "Shortcut+A" )
+ .setAction( e -> getActiveEditorPane().selectAll() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ final Action editFindAction = new ActionBuilder()
+ .setText( "Main.menu.edit.find" )
+ .setAccelerator( "Ctrl+F" )
+ .setIcon( SEARCH )
+ .setAction( e -> editFind() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editFindNextAction = new ActionBuilder()
+ .setText( "Main.menu.edit.find.next" )
+ .setAccelerator( "F3" )
+ .setIcon( null )
+ .setAction( e -> editFindNext() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action editPreferencesAction = new ActionBuilder()
+ .setText( "Main.menu.edit.preferences" )
+ .setAccelerator( "Ctrl+Alt+S" )
+ .setAction( e -> editPreferences() )
+ .build();
+
+ // Insert actions
+ final Action insertBoldAction = new ActionBuilder()
+ .setText( "Main.menu.insert.bold" )
+ .setAccelerator( "Shortcut+B" )
+ .setIcon( BOLD )
+ .setAction( e -> insertMarkdown( "**", "**" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertItalicAction = new ActionBuilder()
+ .setText( "Main.menu.insert.italic" )
+ .setAccelerator( "Shortcut+I" )
+ .setIcon( ITALIC )
+ .setAction( e -> insertMarkdown( "*", "*" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertSuperscriptAction = new ActionBuilder()
+ .setText( "Main.menu.insert.superscript" )
+ .setAccelerator( "Shortcut+[" )
+ .setIcon( SUPERSCRIPT )
+ .setAction( e -> insertMarkdown( "^", "^" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertSubscriptAction = new ActionBuilder()
+ .setText( "Main.menu.insert.subscript" )
+ .setAccelerator( "Shortcut+]" )
+ .setIcon( SUBSCRIPT )
+ .setAction( e -> insertMarkdown( "~", "~" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertStrikethroughAction = new ActionBuilder()
+ .setText( "Main.menu.insert.strikethrough" )
+ .setAccelerator( "Shortcut+T" )
+ .setIcon( STRIKETHROUGH )
+ .setAction( e -> insertMarkdown( "~~", "~~" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertBlockquoteAction = new ActionBuilder()
+ .setText( "Main.menu.insert.blockquote" )
+ .setAccelerator( "Ctrl+Q" )
+ .setIcon( QUOTE_LEFT )
+ .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertCodeAction = new ActionBuilder()
+ .setText( "Main.menu.insert.code" )
+ .setAccelerator( "Shortcut+K" )
+ .setIcon( CODE )
+ .setAction( e -> insertMarkdown( "`", "`" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertFencedCodeBlockAction = new ActionBuilder()
+ .setText( "Main.menu.insert.fenced_code_block" )
+ .setAccelerator( "Shortcut+Shift+K" )
+ .setIcon( FILE_CODE_ALT )
+ .setAction( e -> insertMarkdown(
+ "\n\n```\n",
+ "\n```\n\n",
+ get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertLinkAction = new ActionBuilder()
+ .setText( "Main.menu.insert.link" )
+ .setAccelerator( "Shortcut+L" )
+ .setIcon( LINK )
+ .setAction( e -> getActiveEditorPane().insertLink() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertImageAction = new ActionBuilder()
+ .setText( "Main.menu.insert.image" )
+ .setAccelerator( "Shortcut+G" )
+ .setIcon( PICTURE_ALT )
+ .setAction( e -> getActiveEditorPane().insertImage() )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ // Number of header actions (H1 ... H3)
+ final int HEADERS = 3;
+ final Action[] headers = new Action[ HEADERS ];
+
+ for( int i = 1; i <= HEADERS; i++ ) {
+ final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
+ final String markup = String.format( "%n%n%s ", hashes );
+ final String text = "Main.menu.insert.header." + i;
+ final String accelerator = "Shortcut+" + i;
+ final String prompt = text + ".prompt";
+
+ headers[ i - 1 ] = new ActionBuilder()
+ .setText( text )
+ .setAccelerator( accelerator )
+ .setIcon( HEADER )
+ .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ }
+
+ final Action insertUnorderedListAction = new ActionBuilder()
+ .setText( "Main.menu.insert.unordered_list" )
+ .setAccelerator( "Shortcut+U" )
+ .setIcon( LIST_UL )
+ .setAction( e -> insertMarkdown( "\n\n* ", "" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertOrderedListAction = new ActionBuilder()
+ .setText( "Main.menu.insert.ordered_list" )
+ .setAccelerator( "Shortcut+Shift+O" )
+ .setIcon( LIST_OL )
+ .setAction( e -> insertMarkdown(
+ "\n\n1. ", "" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+ final Action insertHorizontalRuleAction = new ActionBuilder()
+ .setText( "Main.menu.insert.horizontal_rule" )
+ .setAccelerator( "Shortcut+H" )
+ .setAction( e -> insertMarkdown(
+ "\n\n---\n\n", "" ) )
+ .setDisable( activeFileEditorIsNull )
+ .build();
+
+ // Help actions
+ final Action helpAboutAction = new ActionBuilder()
+ .setText( "Main.menu.help.about" )
+ .setAction( e -> helpAbout() )
+ .build();
+
+ //---- MenuBar ----
+ final Menu fileMenu = ActionUtils.createMenu(
+ get( "Main.menu.file" ),
+ fileNewAction,
+ fileOpenAction,
+ null,
+ fileCloseAction,
+ fileCloseAllAction,
+ null,
+ fileSaveAction,
+ fileSaveAsAction,
+ fileSaveAllAction,
+ null,
+ fileExitAction );
+
+ final Menu editMenu = ActionUtils.createMenu(
+ get( "Main.menu.edit" ),
+ editCopyHtmlAction,
+ null,
+ editUndoAction,
+ editRedoAction,
+ null,
+ editCutAction,
+ editCopyAction,
+ editPasteAction,
+ editSelectAllAction,
+ null,
+ editFindAction,
+ editFindNextAction,
+ null,
+ editPreferencesAction );
+
+ final Menu insertMenu = ActionUtils.createMenu(
+ get( "Main.menu.insert" ),
+ insertBoldAction,
+ insertItalicAction,
+ insertSuperscriptAction,
+ insertSubscriptAction,
+ insertStrikethroughAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headers[ 0 ],
+ headers[ 1 ],
+ headers[ 2 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction,
+ insertHorizontalRuleAction
+ );
+
+ final Menu helpMenu = ActionUtils.createMenu(
+ get( "Main.menu.help" ),
+ helpAboutAction );
+
+ final MenuBar menuBar = new MenuBar(
+ fileMenu,
+ editMenu,
+ insertMenu,
+ helpMenu );
+
+ //---- ToolBar ----
+ final ToolBar toolBar = ActionUtils.createToolBar(
+ fileNewAction,
+ fileOpenAction,
+ fileSaveAction,
+ null,
+ editUndoAction,
+ editRedoAction,
+ editCutAction,
+ editCopyAction,
+ editPasteAction,
+ null,
+ insertBoldAction,
+ insertItalicAction,
+ insertSuperscriptAction,
+ insertSubscriptAction,
+ insertBlockquoteAction,
+ insertCodeAction,
+ insertFencedCodeBlockAction,
+ null,
+ insertLinkAction,
+ insertImageAction,
+ null,
+ headers[ 0 ],
+ null,
+ insertUnorderedListAction,
+ insertOrderedListAction );
+
+ return new VBox( menuBar, toolBar );
+ }
+
+ /**
+ * Creates a boolean property that is bound to another boolean value of the
+ * active editor.
+ */
+ private BooleanProperty createActiveBooleanProperty(
+ final Function<FileEditorTab, ObservableBooleanValue> func ) {
+
+ final BooleanProperty b = new SimpleBooleanProperty();
+ final FileEditorTab tab = getActiveFileEditorTab();
+
+ if( tab != null ) {
+ b.bind( func.apply( tab ) );
+ }
+
+ getFileEditorPane().activeFileEditorProperty().addListener(
+ ( observable, oldFileEditor, newFileEditor ) -> {
+ b.unbind();
+
+ if( newFileEditor == null ) {
+ b.set( false );
+ }
+ else {
+ b.bind( func.apply( newFileEditor ) );
+ }
+ }
+ );
+
+ return b;
+ }
+
+ //---- Convenience accessors ----------------------------------------------
+
+ private Preferences getPreferences() {
+ return OPTIONS.getState();
+ }
+
+ private float getFloat( final String key, final float defaultValue ) {
+ return getPreferences().getFloat( key, defaultValue );
+ }
+
+ public Window getWindow() {
+ return getScene().getWindow();
+ }
+
+ private MarkdownEditorPane getActiveEditorPane() {
+ return getActiveFileEditorTab().getEditorPane();
+ }
+
+ private FileEditorTab getActiveFileEditorTab() {
+ return getFileEditorPane().getActiveFileEditor();
+ }
+
+ //---- Member accessors ---------------------------------------------------
+
+ protected Scene getScene() {
+ return mScene;
+ }
+
+ private SpellChecker getSpellChecker() {
+ return mSpellChecker;
+ }
+
+ private Map<FileEditorTab, Processor<String>> getProcessors() {
+ return mProcessors;
+ }
+
+ private FileEditorTabPane getFileEditorPane() {
+ return mFileEditorPane;
+ }
+
+ private HTMLPreviewPane getPreviewPane() {
+ return mPreviewPane;
+ }
+
+ private void setDefinitionSource(
+ final DefinitionSource definitionSource ) {
+ assert definitionSource != null;
+ mDefinitionSource = definitionSource;
+ }
+
+ private DefinitionSource getDefinitionSource() {
+ return mDefinitionSource;
+ }
+
+ private DefinitionPane getDefinitionPane() {
+ return mDefinitionPane;
+ }
+
+ private Text getLineNumberText() {
+ return mLineNumberText;
+ }
+
+ private StatusBar getStatusBar() {
+ return mStatusBar;
+ }
+
+ private TextField getFindTextField() {
+ return mFindTextField;
+ }
+
+ private VariableNameInjector getVariableNameInjector() {
+ return mVariableNameInjector;
+ }
+
+ /**
+ * Returns the variable map of interpolated definitions.
+ *
+ * @return A map to help dereference variables.
+ */
+ private Map<String, String> getResolvedMap() {
+ return mResolvedMap;
+ }
+
+ private Notifier getNotifier() {
+ return NOTIFIER;
+ }
+
+ //---- Persistence accessors ----------------------------------------------
+
+ private UserPreferences getUserPreferences() {
+ return OPTIONS.getUserPreferences();
+ }
+
+ private Path getDefinitionPath() {
+ return getUserPreferences().getDefinitionPath();
+ }
+
+ //---- Resource accessors -------------------------------------------------
+
+ @SuppressWarnings("SameParameterValue")
+ private Collection<String> readLexicon( final String filename )
+ throws Exception {
+ final var path = Paths.get( LEXICONS_DIRECTORY, filename ).toString();
+ final var classLoader = MainWindow.class.getClassLoader();
+
+ try( final var resource = classLoader.getResourceAsStream( path ) ) {
+ assert resource != null;
+
+ return new BufferedReader( new InputStreamReader( resource, UTF_8 ) )
+ .lines()
+ .collect( Collectors.toList() );
+ }
}
}
src/main/java/com/scrivenvar/spelling/api/SpellChecker.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.spelling.api;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * Defines the responsibilities for a spell checking API. The intention is
+ * to allow different spell checking implementations to be used by the
+ * application, such as SymSpell and LinSpell.
+ */
+public interface SpellChecker {
+
+ /**
+ * Answers whether the given lexeme, in whole, is found in the lexicon. The
+ * lexicon lookup is performed case-insensitively. This method should be
+ * used instead of {@link #suggestions(String, int)} for performance reasons.
+ *
+ * @param lexeme The word to check for correctness.
+ * @return {@code true} if the lexeme is in the lexicon.
+ */
+ boolean inLexicon( String lexeme );
+
+ /**
+ * Gets a list of spelling corrections for the given lexeme.
+ *
+ * @param lexeme A word to check for correctness that's not in the lexicon.
+ * @param count The maximum number of alternatives to return.
+ * @return A list of words in the lexicon that are similar to the given
+ * lexeme.
+ */
+ List<String> suggestions( String lexeme, int count );
+
+ /**
+ * Iterates over the given text, emitting starting and ending offsets into
+ * the text for every word that is missing from the lexicon.
+ *
+ * @param text The text to check for words missing from the lexicon.
+ * @param consumer Every missing word emits a message with the starting
+ * and ending offset into the text where said word is found.
+ */
+ void proofread( String text, BiConsumer<Integer, Integer> consumer );
+}
src/main/java/com/scrivenvar/spelling/impl/PermissiveSpeller.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.spelling.impl;
+
+import com.scrivenvar.spelling.api.SpellChecker;
+
+import java.util.List;
+import java.util.function.BiConsumer;
+
+/**
+ * Responsible for spell checking in the event that a real spell checking
+ * implementation cannot be created (for any reason). Does not perform any
+ * spell checking and indicates that any given lexeme is in the lexicon.
+ */
+public class PermissiveSpeller implements SpellChecker {
+ /**
+ * Returns {@code true}, ignoring the given word.
+ *
+ * @param ignored Unused.
+ * @return {@code true}
+ */
+ @Override
+ public boolean inLexicon( final String ignored ) {
+ return true;
+ }
+
+ /**
+ * Returns an array with the given lexeme.
+ *
+ * @param lexeme The word to return.
+ * @param ignored Unused.
+ * @return A suggestion list containing the given lexeme.
+ */
+ @Override
+ public List<String> suggestions( final String lexeme, final int ignored ) {
+ return List.of( lexeme );
+ }
+
+ /**
+ * Performs no action.
+ *
+ * @param text Unused.
+ * @param ignored Uncalled.
+ */
+ @Override
+ public void proofread(
+ final String text, final BiConsumer<Integer, Integer> ignored ) {
+ }
+}
src/main/java/com/scrivenvar/spelling/impl/SymSpellSpeller.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.spelling.impl;
+
+import com.scrivenvar.spelling.api.SpellChecker;
+import io.gitlab.rxp90.jsymspell.SuggestItem;
+import io.gitlab.rxp90.jsymspell.SymSpell;
+import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
+
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.BiConsumer;
+
+import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
+import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
+import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
+import static java.lang.Character.isLetter;
+
+/**
+ * Responsible for spell checking using {@link SymSpell}.
+ */
+public class SymSpellSpeller implements SpellChecker {
+ private final SymSpell mSymSpell;
+
+ /**
+ * Creates a new lexicon for the given collection of lexemes.
+ *
+ * @param lexiconWords The words in the lexicon to add for spell checking,
+ * must not be empty.
+ * @return An instance of {@link SpellChecker} that can check if a word
+ * is correct and suggest alternatives.
+ */
+ public static SpellChecker forLexicon(
+ final Collection<String> lexiconWords ) {
+ assert lexiconWords != null && !lexiconWords.isEmpty();
+
+ final SymSpellBuilder builder = new SymSpellBuilder()
+ .setLexiconWords( lexiconWords );
+
+ return new SymSpellSpeller( builder.build() );
+ }
+
+ /**
+ * Prevent direct instantiation so that only the {@link SpellChecker}
+ * interface
+ * is available.
+ *
+ * @param symSpell The implementation-specific spell checker.
+ */
+ private SymSpellSpeller( final SymSpell symSpell ) {
+ mSymSpell = symSpell;
+ }
+
+ @Override
+ public boolean inLexicon( final String lexeme ) {
+ return lookup( lexeme, CLOSEST ).size() == 1;
+ }
+
+ @Override
+ public List<String> suggestions( final String lexeme, int count ) {
+ final List<String> result = new ArrayList<>( count );
+
+ for( final var item : lookup( lexeme, ALL ) ) {
+ if( count-- > 0 ) {
+ result.add( item.getSuggestion() );
+ }
+ else {
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public void proofread(
+ final String text, final BiConsumer<Integer, Integer> consumer ) {
+ assert text != null;
+ assert consumer != null;
+
+ final BreakIterator wb = BreakIterator.getWordInstance();
+ wb.setText( text );
+
+ int boundaryIndex = wb.first();
+ int previousIndex = 0;
+
+ while( boundaryIndex != BreakIterator.DONE ) {
+ final String substring =
+ text.substring( previousIndex, boundaryIndex ).toLowerCase();
+
+ if( isWord( substring ) && !inLexicon( substring ) ) {
+ consumer.accept( previousIndex, boundaryIndex );
+ }
+
+ previousIndex = boundaryIndex;
+ boundaryIndex = wb.next();
+ }
+ }
+
+ /**
+ * Answers whether the given string is likely a word by checking the first
+ * character.
+ *
+ * @param word The word to check.
+ * @return {@code true} if the word begins with a letter.
+ */
+ private boolean isWord( final String word ) {
+ return !word.isEmpty() && isLetter( word.charAt( 0 ) );
+ }
+
+ /**
+ * Returns a list of {@link SuggestItem} instances that provide alternative
+ * spellings for the given lexeme.
+ *
+ * @param lexeme A word to look up in the lexicon.
+ * @param v Influences the number of results returned.
+ * @return Alternative lexemes.
+ */
+ private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
+ return getSpeller().lookup( lexeme, v );
+ }
+
+ private SymSpell getSpeller() {
+ return mSymSpell;
+ }
+}
src/main/resources/com/scrivenvar/editor/markdown.css
.markdown-editor {
- -fx-font-size: 14px;
+ -fx-font-size: 11pt;
}
.markdown-editor .selection {
-fx-fill: #a6d2ff;
+}
+
+/* Decoration for words not found in the lexicon. */
+.markdown-editor .spelling {
+ -rtfx-underline-color: rgba(255, 131, 67, .9);
+ -rtfx-underline-dash-array: 4, 2;
+ -rtfx-underline-width: 2;
}
src/main/resources/com/scrivenvar/messages.properties
# ########################################################################
-# Browse Directory
-# ########################################################################
-
-BrowseDirectoryButton.chooser.title=Browse for local folder
-BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title}
-
-# ########################################################################
# Browse File
# ########################################################################
Dialog.about.header=${Main.title}
Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
-
-# Options ################################################################
-
-# ########################################################################
-# Options Dialog
-# ########################################################################
-
-OptionsDialog.title=Options
-OptionsDialog.generalTab.text=General
-OptionsDialog.markdownTab.text=Markdown
-
-# ########################################################################
-# General Options Pane
-# ########################################################################
-
-GeneralOptionsPane.encodingLabel.text=En_coding\:
-GeneralOptionsPane.lineSeparatorLabel.text=_Line separator\:
-GeneralOptionsPane.lineSeparatorLabel2.text=(applies to new files only)
-
-GeneralOptionsPane.platformDefault=Platform Default ({0})
-GeneralOptionsPane.sepWindows=Windows (CRLF)
-GeneralOptionsPane.sepUnix=Unix (LF)
-
-# ########################################################################
-# Markdown Options Pane
-# ########################################################################
-
-MarkdownOptionsPane.abbreviationsExtCheckBox.text=A_bbreviations in the way of
-MarkdownOptionsPane.abbreviationsExtLink.text=Markdown Extra
-MarkdownOptionsPane.anchorlinksExtCheckBox.text=_Anchor links in headers
-MarkdownOptionsPane.atxHeaderSpaceExtCheckBox.text=Requires a space char after Atx \# header prefixes, so that \#dasdsdaf is not a header
-MarkdownOptionsPane.autolinksExtCheckBox.text=_Plain (undelimited) autolinks in the way of
-MarkdownOptionsPane.autolinksExtLink.text=Github-flavoured-Markdown
-MarkdownOptionsPane.definitionListsExtCheckBox.text=_Definition lists in the way of
-MarkdownOptionsPane.definitionListsExtLink.text=Markdown Extra
-MarkdownOptionsPane.extAnchorLinksExtCheckBox.text=Generate anchor links for headers using complete contents of the header
-MarkdownOptionsPane.fencedCodeBlocksExtCheckBox.text=_Fenced Code Blocks in the way of
-MarkdownOptionsPane.fencedCodeBlocksExtLabel.text=or
-MarkdownOptionsPane.fencedCodeBlocksExtLink.text=Markdown Extra
-MarkdownOptionsPane.fencedCodeBlocksExtLink2.text=Github-flavoured-Markdown
-MarkdownOptionsPane.forceListItemParaExtCheckBox.text=Force List and Definition Paragraph wrapping if it includes more than just a single paragraph
-MarkdownOptionsPane.hardwrapsExtCheckBox.text=_Newlines in paragraph-like content as real line breaks, see
-MarkdownOptionsPane.hardwrapsExtLink.text=Github-flavoured-Markdown
-MarkdownOptionsPane.quotesExtCheckBox.text=Beautify single _quotes, double quotes and double angle quotes (\u00ab and \u00bb)
-MarkdownOptionsPane.relaxedHrRulesExtCheckBox.text=Allow horizontal rules without a blank line following them
-MarkdownOptionsPane.smartsExtCheckBox.text=Beautify apostrophes, _ellipses ("..." and ". . .") and dashes ("--" and "---")
-MarkdownOptionsPane.strikethroughExtCheckBox.text=_Strikethrough
-MarkdownOptionsPane.suppressHtmlBlocksExtCheckBox.text=Suppress the _output of HTML blocks
-MarkdownOptionsPane.suppressInlineHtmlExtCheckBox.text=Suppress the o_utput of inline HTML elements
-MarkdownOptionsPane.tablesExtCheckBox.text=_Tables similar to
-MarkdownOptionsPane.tablesExtLabel.text=(like
-MarkdownOptionsPane.tablesExtLabel2.text=tables, but with colspan support)
-MarkdownOptionsPane.tablesExtLink.text=MultiMarkdown
-MarkdownOptionsPane.tablesExtLink2.text=Markdown Extra
-MarkdownOptionsPane.taskListItemsExtCheckBox.text=GitHub style task list items
-MarkdownOptionsPane.wikilinksExtCheckBox.text=_Wiki-style links ("[[wiki link]]")
src/main/resources/lexicons/README.md
+# Building
+
+The lexicon files are retrieved from SymSpell in the parent directory:
+
+svn export \
+ https://github.com/wolfgarbe/SymSpell/trunk/SymSpell.FrequencyDictionary/ lexicons
+
+The lexicons and bigrams are both space-separated, but parsing a
+tab-delimited file is easier, so change them to tab-separated files.
src/main/resources/lexicons/de.txt
Binary files differ
src/main/resources/lexicons/en.txt
Binary files differ
src/main/resources/lexicons/es.txt
Binary files differ
src/main/resources/lexicons/fr.txt
Binary files differ
src/main/resources/lexicons/he.txt
Binary files differ
src/main/resources/lexicons/it.txt
Binary files differ
src/main/resources/lexicons/ru.txt
Binary files differ
src/main/resources/lexicons/zh.txt
Binary files differ
Delta1658 lines added, 1309 lines removed, 349-line increase