Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
build.gradle
// Misc.
implementation 'org.ahocorasick:ahocorasick:0.6.3'
- implementation 'org.apache.commons:commons-lang3:3.14.0'
implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
src/main/java/com/keenwrite/constants/Constants.java
import static com.keenwrite.io.SysFile.toFile;
import static com.keenwrite.preferences.LocaleScripts.withScript;
+import static com.keenwrite.util.SystemUtils.*;
import static java.io.File.separator;
import static java.lang.String.format;
import static java.lang.System.getProperty;
-import static org.apache.commons.lang3.SystemUtils.*;
/**
src/main/java/com/keenwrite/editors/markdown/MarkdownEditor.java
import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN;
import static com.keenwrite.preferences.AppKeys.*;
-import static java.lang.Character.isWhitespace;
-import static java.lang.String.format;
-import static java.util.Collections.singletonList;
-import static javafx.application.Platform.runLater;
-import static javafx.scene.control.ScrollPane.ScrollBarPolicy.ALWAYS;
-import static javafx.scene.input.KeyCode.*;
-import static javafx.scene.input.KeyCombination.*;
-import static org.apache.commons.lang3.StringUtils.stripEnd;
-import static org.apache.commons.lang3.StringUtils.stripStart;
-import static org.fxmisc.richtext.Caret.CaretVisibility.ON;
-import static org.fxmisc.richtext.model.StyleSpans.singleton;
-import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
-import static org.fxmisc.wellbehaved.event.InputMap.consume;
-
-/**
- * Responsible for editing Markdown documents.
- */
-public final class MarkdownEditor extends BorderPane implements TextEditor {
- /**
- * Regular expression that matches the type of markup block. This is used
- * when Enter is pressed to continue the block environment.
- */
- private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
- "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
-
- private final Workspace mWorkspace;
-
- /**
- * The text editor.
- */
- private final StyleClassedTextArea mTextArea =
- new StyleClassedTextArea( false );
-
- /**
- * Wraps the text editor in scrollbars.
- */
- private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
- new VirtualizedScrollPane<>( mTextArea );
-
- /**
- * Tracks where the caret is located in this document. This offers observable
- * properties for caret position changes.
- */
- private final Caret mCaret = createCaret( mTextArea );
-
- /**
- * File being edited by this editor instance.
- */
- private File mFile;
-
- /**
- * Set to {@code true} upon text or caret position changes. Value is {@code
- * false} by default.
- */
- private final BooleanProperty mDirty = new SimpleBooleanProperty();
-
- /**
- * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
- * either no encoding could be determined or this is a new (empty) file.
- */
- private final Charset mEncoding;
-
- /**
- * Tracks whether the in-memory definitions have changed with respect to the
- * persisted definitions.
- */
- private final BooleanProperty mModified = new SimpleBooleanProperty();
-
- public MarkdownEditor( final File file, final Workspace workspace ) {
- mEncoding = open( mFile = file );
- mWorkspace = workspace;
-
- initTextArea( mTextArea );
- initStyle( mTextArea );
- initScrollPane( mScrollPane );
- initHotKeys();
- initUndoManager();
- }
-
- @SuppressWarnings( "unused" )
- private void initTextArea( final StyleClassedTextArea textArea ) {
- textArea.setShowCaret( ON );
- textArea.setWrapText( true );
- textArea.requestFollowCaret();
- textArea.moveTo( 0 );
-
- textArea.textProperty().addListener( ( c, o, n ) -> {
- // Fire, regardless of whether the caret position has changed.
- mDirty.set( false );
-
- // Prevent the subsequent caret position change from raising dirty bits.
- mDirty.set( true );
- } );
-
- textArea.caretPositionProperty().addListener( ( c, o, n ) -> {
- // Fire when the caret position has changed and the text has not.
- mDirty.set( true );
- mDirty.set( false );
- } );
-
- textArea.focusedProperty().addListener( ( c, o, n ) -> {
- if( n != null && n ) {
- TextEditorFocusEvent.fire( this );
- }
- } );
- }
-
- @SuppressWarnings( "unused" )
- private void initStyle( final StyleClassedTextArea textArea ) {
- textArea.getStyleClass().add( "markdown" );
-
- final var stylesheets = textArea.getStylesheets();
- stylesheets.add( getStylesheetPath( getLocale() ) );
-
- localeProperty().addListener( ( c, o, n ) -> {
- if( n != null ) {
- stylesheets.clear();
- stylesheets.add( getStylesheetPath( getLocale() ) );
- }
- } );
-
- fontNameProperty().addListener(
- ( c, o, n ) ->
- setFont( mTextArea, getFontName(), getFontSize() )
- );
-
- fontSizeProperty().addListener(
- ( c, o, n ) ->
- setFont( mTextArea, getFontName(), getFontSize() )
- );
-
- setFont( mTextArea, getFontName(), getFontSize() );
- }
-
- private void initScrollPane(
- final VirtualizedScrollPane<StyleClassedTextArea> scrollpane ) {
- scrollpane.setVbarPolicy( ALWAYS );
- setCenter( scrollpane );
- }
-
- private void initHotKeys() {
- addEventListener( keyPressed( ENTER ), this::onEnterPressed );
- addEventListener( keyPressed( X, CONTROL_DOWN ), this::cut );
- addEventListener( keyPressed( TAB ), this::tab );
- addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab );
- }
-
- private void initUndoManager() {
- final var undoManager = getUndoManager();
- final var markedPosition = undoManager.atMarkedPositionProperty();
-
- undoManager.forgetHistory();
- undoManager.mark();
- mModified.bind( Bindings.not( markedPosition ) );
- }
-
- @Override
- public void moveTo( final int offset ) {
- assert 0 <= offset && offset <= mTextArea.getLength();
-
- if( offset <= mTextArea.getLength() ) {
- mTextArea.moveTo( offset );
- mTextArea.requestFollowCaret();
- }
- }
-
- /**
- * Delegate the focus request to the text area itself.
- */
- @Override
- public void requestFocus() {
- mTextArea.requestFocus();
- }
-
- @Override
- public void setText( final String text ) {
- mTextArea.clear();
- mTextArea.appendText( text );
- mTextArea.getUndoManager().mark();
- }
-
- @Override
- public String getText() {
- return mTextArea.getText();
- }
-
- @Override
- public Charset getEncoding() {
- return mEncoding;
- }
-
- @Override
- public File getFile() {
- return mFile;
- }
-
- @Override
- public void rename( final File file ) {
- mFile = file;
- }
-
- @Override
- public void undo() {
- final var manager = getUndoManager();
- xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
- }
-
- @Override
- public void redo() {
- final var manager = getUndoManager();
- xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
- }
-
- /**
- * Performs an undo or redo action, if possible, otherwise displays an error
- * message to the user.
- *
- * @param ready Answers whether the action can be executed.
- * @param action The action to execute.
- * @param key The informational message key having a value to display if
- * the {@link Supplier} is not ready.
- */
- private void xxdo(
- final Supplier<Boolean> ready, final Runnable action, final String key ) {
- if( ready.get() ) {
- action.run();
- }
- else {
- clue( key );
- }
- }
-
- @Override
- public void cut() {
- final var selected = mTextArea.getSelectedText();
-
- // Emulate selecting the current line by firing Home then Shift+Down Arrow.
- if( selected == null || selected.isEmpty() ) {
- // Note: mTextArea.selectLine() does not select empty lines.
- mTextArea.fireEvent( keyDown( HOME, false ) );
- mTextArea.fireEvent( keyDown( DOWN, true ) );
- }
-
- mTextArea.cut();
- }
-
- @Override
- public void copy() {
- mTextArea.copy();
- }
-
- @Override
- public void paste() {
- mTextArea.paste();
- }
-
- @Override
- public void selectAll() {
- mTextArea.selectAll();
- }
-
- @Override
- public void bold() {
- enwrap( "**" );
- }
-
- @Override
- public void italic() {
- enwrap( "*" );
- }
-
- @Override
- public void monospace() {
- enwrap( "`" );
- }
-
- @Override
- public void superscript() {
- enwrap( "^" );
- }
-
- @Override
- public void subscript() {
- enwrap( "~" );
- }
-
- @Override
- public void strikethrough() {
- enwrap( "~~" );
- }
-
- @Override
- public void blockquote() {
- block( "> " );
- }
-
- @Override
- public void code() {
- enwrap( "`" );
- }
-
- @Override
- public void fencedCodeBlock() {
- enwrap( "\n\n```\n", "\n```\n\n" );
- }
-
- @Override
- public void heading( final int level ) {
- final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
- block( format( "%s ", hashes ) );
- }
-
- @Override
- public void unorderedList() {
- block( "* " );
- }
-
- @Override
- public void orderedList() {
- block( "1. " );
- }
-
- @Override
- public void horizontalRule() {
- block( format( "---%n%n" ) );
- }
-
- @Override
- public Node getNode() {
- return this;
- }
-
- @Override
- public ReadOnlyBooleanProperty modifiedProperty() {
- return mModified;
- }
-
- @Override
- public void clearModifiedProperty() {
- getUndoManager().mark();
- }
-
- @Override
- public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
- return mScrollPane;
- }
-
- @Override
- public StyleClassedTextArea getTextArea() {
- return mTextArea;
- }
-
- private final Map<String, IndexRange> mStyles = new HashMap<>();
-
- @Override
- public void stylize( final IndexRange range, final String style ) {
- final var began = range.getStart();
- final var ended = range.getEnd() + 1;
-
- assert 0 <= began && began <= ended;
- assert style != null;
-
- // TODO: Ensure spell check and find highlights can coexist.
-// final var spans = mTextArea.getStyleSpans( range );
-// System.out.println( "SPANS: " + spans );
-
-// final var spans = mTextArea.getStyleSpans( range );
-// mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style
-// ) );
-
-// final var builder = new StyleSpansBuilder<Collection<String>>();
-// builder.add( singleton( style ), range.getLength() + 1 );
-// mTextArea.setStyleSpans( began, builder.create() );
-
-// final var s = mTextArea.getStyleSpans( began, ended );
-// System.out.println( "STYLES: " +s );
-
- mStyles.put( style, range );
- mTextArea.setStyleClass( began, ended, style );
-
- // Ensure that whenever the user interacts with the text that the found
- // word will have its highlighting removed. The handler removes itself.
- // This won't remove the highlighting if the caret position moves by mouse.
- final var handler = mTextArea.getOnKeyPressed();
- mTextArea.setOnKeyPressed( event -> {
- mTextArea.setOnKeyPressed( handler );
- unstylize( style );
- } );
-
- //mTextArea.setStyleSpans(began, ended, s);
- }
-
- private static StyleSpans<Collection<String>> merge(
- StyleSpans<Collection<String>> spans, int len, String style ) {
- spans = spans.overlay(
- singleton( singletonList( style ), len ),
- ( bottomSpan, list ) -> {
- final List<String> l =
- new ArrayList<>( bottomSpan.size() + list.size() );
- l.addAll( bottomSpan );
- l.addAll( list );
- return l;
- } );
-
- return spans;
- }
-
- @Override
- public void unstylize( final String style ) {
- final var indexes = mStyles.remove( style );
- if( indexes != null ) {
- mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 );
- }
- }
-
- @Override
- public Caret getCaret() {
- return mCaret;
- }
-
- /**
- * A {@link Caret} instance is not directly coupled ot the GUI because
- * document processing does not always require interactive status bar
- * updates. This can happen when processing from the command-line. However,
- * the processors need the {@link Caret} instance to inject the caret
- * position into the document. Making the {@link CaretExtension} optional
- * would require more effort than using a {@link Caret} model that is
- * decoupled from GUI widgets.
- *
- * @param editor The text editor containing caret position information.
- * @return An instance of {@link Caret} that tracks the GUI caret position.
- */
- private Caret createCaret( final StyleClassedTextArea editor ) {
- return Caret
- .builder()
- .with( Caret.Mutator::setParagraph,
- () -> editor.currentParagraphProperty().getValue() )
- .with( Caret.Mutator::setParagraphs,
- () -> editor.getParagraphs().size() )
- .with( Caret.Mutator::setParaOffset,
- () -> editor.caretColumnProperty().getValue() )
- .with( Caret.Mutator::setTextOffset,
- () -> editor.caretPositionProperty().getValue() )
- .with( Caret.Mutator::setTextLength,
- () -> editor.lengthProperty().getValue() )
- .build();
- }
-
- /**
- * This method adds listeners to editor events.
- *
- * @param <T> The event type.
- * @param <U> The consumer type for the given event type.
- * @param event The event of interest.
- * @param consumer The method to call when the event happens.
- */
- public <T extends Event, U extends T> void addEventListener(
- final EventPattern<? super T, ? extends U> event,
- final Consumer<? super U> consumer ) {
- Nodes.addInputMap( mTextArea, consume( event, consumer ) );
- }
-
- private void onEnterPressed( final KeyEvent ignored ) {
- final var currentLine = getCaretParagraph();
- final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
-
- // By default, insert a new line by itself.
- String newText = NEWLINE;
-
- // If the pattern was matched then determine what block type to continue.
- if( matcher.matches() ) {
- if( matcher.group( 2 ).isEmpty() ) {
- final var pos = mTextArea.getCaretPosition();
- mTextArea.selectRange( pos - currentLine.length(), pos );
- }
- else {
- // Indent the new line with the same whitespace characters and
- // list markers as current line. This ensures that the indentation
- // is propagated.
- newText = newText.concat( matcher.group( 1 ) );
- }
- }
-
- mTextArea.replaceSelection( newText );
- mTextArea.requestFollowCaret();
- }
-
- private void cut( final KeyEvent event ) {
- cut();
- }
-
- private void tab( final KeyEvent event ) {
- final var range = mTextArea.selectionProperty().getValue();
- final var sb = new StringBuilder( 1024 );
-
- if( range.getLength() > 0 ) {
- final var selection = mTextArea.getSelectedText();
-
- selection.lines().forEach(
- l -> sb.append( "\t" ).append( l ).append( NEWLINE )
- );
- }
- else {
- sb.append( "\t" );
- }
-
- mTextArea.replaceSelection( sb.toString() );
- }
-
- private void untab( final KeyEvent event ) {
- final var range = mTextArea.selectionProperty().getValue();
-
- if( range.getLength() > 0 ) {
- final var selection = mTextArea.getSelectedText();
- final var sb = new StringBuilder( selection.length() );
-
- selection.lines().forEach(
- l -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
- .append( NEWLINE )
- );
-
- mTextArea.replaceSelection( sb.toString() );
- }
- else {
- final var p = getCaretParagraph();
-
- if( p.startsWith( "\t" ) ) {
- mTextArea.selectParagraph();
- mTextArea.replaceSelection( p.substring( 1 ) );
- }
- }
- }
-
- /**
- * Observers may listen for changes to the property returned from this method
- * to receive notifications when either the text or caret have changed. This
- * should not be used to track whether the text has been modified.
- */
- public void addDirtyListener( ChangeListener<Boolean> listener ) {
- mDirty.addListener( listener );
- }
-
- /**
- * Surrounds the selected text or word under the caret in Markdown markup.
- *
- * @param token The beginning and ending token for enclosing the text.
- */
- private void enwrap( final String token ) {
- enwrap( token, token );
- }
-
- /**
- * Surrounds the selected text or word under the caret in Markdown markup.
- *
- * @param began The beginning token for enclosing the text.
- * @param ended The ending token for enclosing the text.
- */
- private void enwrap( final String began, String ended ) {
- // Ensure selected text takes precedence over the word at caret position.
- final var selected = mTextArea.selectionProperty().getValue();
- final var range = selected.getLength() == 0
- ? getCaretWord()
- : selected;
- String text = mTextArea.getText( range );
-
- int length = range.getLength();
- text = stripStart( text, null );
- final int beganIndex = range.getStart() + length - text.length();
-
- length = text.length();
- text = stripEnd( text, null );
- final int endedIndex = range.getEnd() - (length - text.length());
-
- mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
- }
-
- /**
- * Inserts the given block-level markup at the current caret position
- * within the document. This will prepend two blank lines to ensure that
- * the block element begins at the start of a new line.
- *
- * @param markup The text to insert at the caret.
- */
- private void block( final String markup ) {
- final int pos = mTextArea.getCaretPosition();
- mTextArea.insertText( pos, format( "%n%n%s", markup ) );
- }
-
- /**
- * Returns the caret position within the current paragraph.
- *
- * @return A value from 0 to the length of the current paragraph.
- */
- private int getCaretColumn() {
- return mTextArea.getCaretColumn();
- }
-
- @Override
- public IndexRange getCaretWord() {
- final var paragraph = getCaretParagraph()
- .replaceAll( "---", " " )
- .replaceAll( "--", " " )
- .replaceAll( "[\\[\\]{}()]", " " );
- final var length = paragraph.length();
- final var column = getCaretColumn();
-
- var began = column;
- var ended = column;
-
- while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
- began--;
- }
-
- while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
- ended++;
- }
-
- final var iterator = BreakIterator.getWordInstance();
- iterator.setText( paragraph );
-
- while( began < length && iterator.isBoundary( began + 1 ) ) {
- began++;
- }
-
- while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
- ended--;
- }
-
- final var offset = getCaretDocumentOffset( column );
-
- return IndexRange.normalize( began + offset, ended + offset );
- }
-
- private int getCaretDocumentOffset( final int column ) {
- return mTextArea.getCaretPosition() - column;
- }
-
- /**
- * Returns the index of the paragraph where the caret resides.
- *
- * @return A number greater than or equal to 0.
- */
- private int getCurrentParagraph() {
- return mTextArea.getCurrentParagraph();
- }
-
- /**
- * Returns the text for the paragraph that contains the caret.
- *
- * @return A non-null string, possibly empty.
- */
- private String getCaretParagraph() {
- return getText( getCurrentParagraph() );
- }
-
- @Override
- public String getText( final int paragraph ) {
- return mTextArea.getText( paragraph );
- }
-
- @Override
- public String getText( final IndexRange indexes )
- throws IndexOutOfBoundsException {
- return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
- }
-
- @Override
- public void replaceText( final IndexRange indexes, final String s ) {
- mTextArea.replaceText( indexes, s );
- }
-
- private UndoManager<?> getUndoManager() {
- return mTextArea.getUndoManager();
- }
-
- /**
- * Returns the path to a {@link Locale}-specific stylesheet.
- *
- * @return A non-null string to inject into the HTML document head.
- */
- private static String getStylesheetPath( final Locale locale ) {
- return MessageFormat.format(
- sSettings.getSetting( STYLESHEET_MARKDOWN_LOCALE, "" ),
- locale.getLanguage(),
- locale.getScript(),
- locale.getCountry()
- );
- }
-
- private Locale getLocale() {
- return localeProperty().toLocale();
- }
-
- private LocaleProperty localeProperty() {
- return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
- }
-
- /**
- * Sets the font family name and font size at the same time. When the
- * workspace is loaded, the default font values are changed, which results
- * in this method being called.
- *
- * @param area Change the font settings for this text area.
- * @param name New font family name to apply.
- * @param points New font size to apply (in points, not pixels).
- */
- private void setFont(
- final StyleClassedTextArea area, final String name, final double points ) {
- runLater( () -> area.setStyle(
- format(
- "-fx-font-family:'%s';-fx-font-size:%spx;", name, toPixels( points )
- )
- ) );
- }
-
- private String getFontName() {
- return fontNameProperty().get();
- }
-
- private StringProperty fontNameProperty() {
- return mWorkspace.stringProperty( KEY_UI_FONT_EDITOR_NAME );
- }
-
- private double getFontSize() {
- return fontSizeProperty().get();
- }
-
- private DoubleProperty fontSizeProperty() {
- return mWorkspace.doubleProperty( KEY_UI_FONT_EDITOR_SIZE );
- }
-
- /**
- * Answers whether the given resource is of compatible {@link MediaType}s.
- *
- * @param mediaType The {@link MediaType} to compare.
- * @return {@code true} if the given {@link MediaType} is suitable for
- * editing with this type of editor.
- */
- @Override
- public boolean supports( final MediaType mediaType ) {
- return isMediaType( mediaType ) ||
- mediaType == TEXT_MARKDOWN ||
- mediaType == TEXT_R_MARKDOWN;
+import static com.keenwrite.util.Strings.trimEnd;
+import static com.keenwrite.util.Strings.trimStart;
+import static java.lang.Character.isWhitespace;
+import static java.lang.String.format;
+import static java.util.Collections.singletonList;
+import static javafx.application.Platform.runLater;
+import static javafx.scene.control.ScrollPane.ScrollBarPolicy.ALWAYS;
+import static javafx.scene.input.KeyCode.*;
+import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
+import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
+import static org.fxmisc.richtext.Caret.CaretVisibility.ON;
+import static org.fxmisc.richtext.model.StyleSpans.singleton;
+import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
+import static org.fxmisc.wellbehaved.event.InputMap.consume;
+
+/**
+ * Responsible for editing Markdown documents.
+ */
+public final class MarkdownEditor extends BorderPane implements TextEditor {
+ /**
+ * Represents a failed index search.
+ */
+ private static final int INDEX_NOT_FOUND = -1;
+
+ /**
+ * Regular expression that matches the type of markup block. This is used
+ * when Enter is pressed to continue the block environment.
+ */
+ private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
+ "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
+
+ private final Workspace mWorkspace;
+
+ /**
+ * The text editor.
+ */
+ private final StyleClassedTextArea mTextArea =
+ new StyleClassedTextArea( false );
+
+ /**
+ * Wraps the text editor in scrollbars.
+ */
+ private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
+ new VirtualizedScrollPane<>( mTextArea );
+
+ /**
+ * Tracks where the caret is located in this document. This offers observable
+ * properties for caret position changes.
+ */
+ private final Caret mCaret = createCaret( mTextArea );
+
+ /**
+ * File being edited by this editor instance.
+ */
+ private File mFile;
+
+ /**
+ * Set to {@code true} upon text or caret position changes. Value is {@code
+ * false} by default.
+ */
+ private final BooleanProperty mDirty = new SimpleBooleanProperty();
+
+ /**
+ * Opened file's character encoding, or {@link Constants#DEFAULT_CHARSET} if
+ * either no encoding could be determined or this is a new (empty) file.
+ */
+ private final Charset mEncoding;
+
+ /**
+ * Tracks whether the in-memory definitions have changed with respect to the
+ * persisted definitions.
+ */
+ private final BooleanProperty mModified = new SimpleBooleanProperty();
+
+ public MarkdownEditor( final File file, final Workspace workspace ) {
+ mEncoding = open( mFile = file );
+ mWorkspace = workspace;
+
+ initTextArea( mTextArea );
+ initStyle( mTextArea );
+ initScrollPane( mScrollPane );
+ initHotKeys();
+ initUndoManager();
+ }
+
+ @SuppressWarnings( "unused" )
+ private void initTextArea( final StyleClassedTextArea textArea ) {
+ textArea.setShowCaret( ON );
+ textArea.setWrapText( true );
+ textArea.requestFollowCaret();
+ textArea.moveTo( 0 );
+
+ textArea.textProperty().addListener( ( c, o, n ) -> {
+ // Fire, regardless of whether the caret position has changed.
+ mDirty.set( false );
+
+ // Prevent the subsequent caret position change from raising dirty bits.
+ mDirty.set( true );
+ } );
+
+ textArea.caretPositionProperty().addListener( ( c, o, n ) -> {
+ // Fire when the caret position has changed and the text has not.
+ mDirty.set( true );
+ mDirty.set( false );
+ } );
+
+ textArea.focusedProperty().addListener( ( c, o, n ) -> {
+ if( n != null && n ) {
+ TextEditorFocusEvent.fire( this );
+ }
+ } );
+ }
+
+ @SuppressWarnings( "unused" )
+ private void initStyle( final StyleClassedTextArea textArea ) {
+ textArea.getStyleClass().add( "markdown" );
+
+ final var stylesheets = textArea.getStylesheets();
+ stylesheets.add( getStylesheetPath( getLocale() ) );
+
+ localeProperty().addListener( ( c, o, n ) -> {
+ if( n != null ) {
+ stylesheets.clear();
+ stylesheets.add( getStylesheetPath( getLocale() ) );
+ }
+ } );
+
+ fontNameProperty().addListener(
+ ( c, o, n ) ->
+ setFont( mTextArea, getFontName(), getFontSize() )
+ );
+
+ fontSizeProperty().addListener(
+ ( c, o, n ) ->
+ setFont( mTextArea, getFontName(), getFontSize() )
+ );
+
+ setFont( mTextArea, getFontName(), getFontSize() );
+ }
+
+ private void initScrollPane(
+ final VirtualizedScrollPane<StyleClassedTextArea> scrollpane ) {
+ scrollpane.setVbarPolicy( ALWAYS );
+ setCenter( scrollpane );
+ }
+
+ private void initHotKeys() {
+ addEventListener( keyPressed( ENTER ), this::onEnterPressed );
+ addEventListener( keyPressed( X, CONTROL_DOWN ), this::cut );
+ addEventListener( keyPressed( TAB ), this::tab );
+ addEventListener( keyPressed( TAB, SHIFT_DOWN ), this::untab );
+ }
+
+ private void initUndoManager() {
+ final var undoManager = getUndoManager();
+ final var markedPosition = undoManager.atMarkedPositionProperty();
+
+ undoManager.forgetHistory();
+ undoManager.mark();
+ mModified.bind( Bindings.not( markedPosition ) );
+ }
+
+ @Override
+ public void moveTo( final int offset ) {
+ assert 0 <= offset && offset <= mTextArea.getLength();
+
+ if( offset <= mTextArea.getLength() ) {
+ mTextArea.moveTo( offset );
+ mTextArea.requestFollowCaret();
+ }
+ }
+
+ /**
+ * Delegate the focus request to the text area itself.
+ */
+ @Override
+ public void requestFocus() {
+ mTextArea.requestFocus();
+ }
+
+ @Override
+ public void setText( final String text ) {
+ mTextArea.clear();
+ mTextArea.appendText( text );
+ mTextArea.getUndoManager().mark();
+ }
+
+ @Override
+ public String getText() {
+ return mTextArea.getText();
+ }
+
+ @Override
+ public Charset getEncoding() {
+ return mEncoding;
+ }
+
+ @Override
+ public File getFile() {
+ return mFile;
+ }
+
+ @Override
+ public void rename( final File file ) {
+ mFile = file;
+ }
+
+ @Override
+ public void undo() {
+ final var manager = getUndoManager();
+ xxdo( manager::isUndoAvailable, manager::undo, "Main.status.error.undo" );
+ }
+
+ @Override
+ public void redo() {
+ final var manager = getUndoManager();
+ xxdo( manager::isRedoAvailable, manager::redo, "Main.status.error.redo" );
+ }
+
+ /**
+ * Performs an undo or redo action, if possible, otherwise displays an error
+ * message to the user.
+ *
+ * @param ready Answers whether the action can be executed.
+ * @param action The action to execute.
+ * @param key The informational message key having a value to display if
+ * the {@link Supplier} is not ready.
+ */
+ private void xxdo(
+ final Supplier<Boolean> ready, final Runnable action, final String key ) {
+ if( ready.get() ) {
+ action.run();
+ }
+ else {
+ clue( key );
+ }
+ }
+
+ @Override
+ public void cut() {
+ final var selected = mTextArea.getSelectedText();
+
+ // Emulate selecting the current line by firing Home then Shift+Down Arrow.
+ if( selected == null || selected.isEmpty() ) {
+ // Note: mTextArea.selectLine() does not select empty lines.
+ mTextArea.fireEvent( keyDown( HOME, false ) );
+ mTextArea.fireEvent( keyDown( DOWN, true ) );
+ }
+
+ mTextArea.cut();
+ }
+
+ @Override
+ public void copy() {
+ mTextArea.copy();
+ }
+
+ @Override
+ public void paste() {
+ mTextArea.paste();
+ }
+
+ @Override
+ public void selectAll() {
+ mTextArea.selectAll();
+ }
+
+ @Override
+ public void bold() {
+ enwrap( "**" );
+ }
+
+ @Override
+ public void italic() {
+ enwrap( "*" );
+ }
+
+ @Override
+ public void monospace() {
+ enwrap( "`" );
+ }
+
+ @Override
+ public void superscript() {
+ enwrap( "^" );
+ }
+
+ @Override
+ public void subscript() {
+ enwrap( "~" );
+ }
+
+ @Override
+ public void strikethrough() {
+ enwrap( "~~" );
+ }
+
+ @Override
+ public void blockquote() {
+ block( "> " );
+ }
+
+ @Override
+ public void code() {
+ enwrap( "`" );
+ }
+
+ @Override
+ public void fencedCodeBlock() {
+ enwrap( "\n\n```\n", "\n```\n\n" );
+ }
+
+ @Override
+ public void heading( final int level ) {
+ final var hashes = new String( new char[ level ] ).replace( "\0", "#" );
+ block( format( "%s ", hashes ) );
+ }
+
+ @Override
+ public void unorderedList() {
+ block( "* " );
+ }
+
+ @Override
+ public void orderedList() {
+ block( "1. " );
+ }
+
+ @Override
+ public void horizontalRule() {
+ block( format( "---%n%n" ) );
+ }
+
+ @Override
+ public Node getNode() {
+ return this;
+ }
+
+ @Override
+ public ReadOnlyBooleanProperty modifiedProperty() {
+ return mModified;
+ }
+
+ @Override
+ public void clearModifiedProperty() {
+ getUndoManager().mark();
+ }
+
+ @Override
+ public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
+ return mScrollPane;
+ }
+
+ @Override
+ public StyleClassedTextArea getTextArea() {
+ return mTextArea;
+ }
+
+ private final Map<String, IndexRange> mStyles = new HashMap<>();
+
+ @Override
+ public void stylize( final IndexRange range, final String style ) {
+ final var began = range.getStart();
+ final var ended = range.getEnd() + 1;
+
+ assert 0 <= began && began <= ended;
+ assert style != null;
+
+ // TODO: Ensure spell check and find highlights can coexist.
+// final var spans = mTextArea.getStyleSpans( range );
+// System.out.println( "SPANS: " + spans );
+
+// final var spans = mTextArea.getStyleSpans( range );
+// mTextArea.setStyleSpans( began, merge( spans, range.getLength(), style
+// ) );
+
+// final var builder = new StyleSpansBuilder<Collection<String>>();
+// builder.add( singleton( style ), range.getLength() + 1 );
+// mTextArea.setStyleSpans( began, builder.create() );
+
+// final var s = mTextArea.getStyleSpans( began, ended );
+// System.out.println( "STYLES: " +s );
+
+ mStyles.put( style, range );
+ mTextArea.setStyleClass( began, ended, style );
+
+ // Ensure that whenever the user interacts with the text that the found
+ // word will have its highlighting removed. The handler removes itself.
+ // This won't remove the highlighting if the caret position moves by mouse.
+ final var handler = mTextArea.getOnKeyPressed();
+ mTextArea.setOnKeyPressed( event -> {
+ mTextArea.setOnKeyPressed( handler );
+ unstylize( style );
+ } );
+
+ //mTextArea.setStyleSpans(began, ended, s);
+ }
+
+ private static StyleSpans<Collection<String>> merge(
+ StyleSpans<Collection<String>> spans, int len, String style ) {
+ spans = spans.overlay(
+ singleton( singletonList( style ), len ),
+ ( bottomSpan, list ) -> {
+ final List<String> l =
+ new ArrayList<>( bottomSpan.size() + list.size() );
+ l.addAll( bottomSpan );
+ l.addAll( list );
+ return l;
+ } );
+
+ return spans;
+ }
+
+ @Override
+ public void unstylize( final String style ) {
+ final var indexes = mStyles.remove( style );
+ if( indexes != null ) {
+ mTextArea.clearStyle( indexes.getStart(), indexes.getEnd() + 1 );
+ }
+ }
+
+ @Override
+ public Caret getCaret() {
+ return mCaret;
+ }
+
+ /**
+ * A {@link Caret} instance is not directly coupled ot the GUI because
+ * document processing does not always require interactive status bar
+ * updates. This can happen when processing from the command-line. However,
+ * the processors need the {@link Caret} instance to inject the caret
+ * position into the document. Making the {@link CaretExtension} optional
+ * would require more effort than using a {@link Caret} model that is
+ * decoupled from GUI widgets.
+ *
+ * @param editor The text editor containing caret position information.
+ * @return An instance of {@link Caret} that tracks the GUI caret position.
+ */
+ private Caret createCaret( final StyleClassedTextArea editor ) {
+ return Caret
+ .builder()
+ .with( Caret.Mutator::setParagraph,
+ () -> editor.currentParagraphProperty().getValue() )
+ .with( Caret.Mutator::setParagraphs,
+ () -> editor.getParagraphs().size() )
+ .with( Caret.Mutator::setParaOffset,
+ () -> editor.caretColumnProperty().getValue() )
+ .with( Caret.Mutator::setTextOffset,
+ () -> editor.caretPositionProperty().getValue() )
+ .with( Caret.Mutator::setTextLength,
+ () -> editor.lengthProperty().getValue() )
+ .build();
+ }
+
+ /**
+ * This method adds listeners to editor events.
+ *
+ * @param <T> The event type.
+ * @param <U> The consumer type for the given event type.
+ * @param event The event of interest.
+ * @param consumer The method to call when the event happens.
+ */
+ public <T extends Event, U extends T> void addEventListener(
+ final EventPattern<? super T, ? extends U> event,
+ final Consumer<? super U> consumer ) {
+ Nodes.addInputMap( mTextArea, consume( event, consumer ) );
+ }
+
+ private void onEnterPressed( final KeyEvent ignored ) {
+ final var currentLine = getCaretParagraph();
+ final var matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
+
+ // By default, insert a new line by itself.
+ String newText = NEWLINE;
+
+ // If the pattern was matched then determine what block type to continue.
+ if( matcher.matches() ) {
+ if( matcher.group( 2 ).isEmpty() ) {
+ final var pos = mTextArea.getCaretPosition();
+ mTextArea.selectRange( pos - currentLine.length(), pos );
+ }
+ else {
+ // Indent the new line with the same whitespace characters and
+ // list markers as current line. This ensures that the indentation
+ // is propagated.
+ newText = newText.concat( matcher.group( 1 ) );
+ }
+ }
+
+ mTextArea.replaceSelection( newText );
+ mTextArea.requestFollowCaret();
+ }
+
+ private void cut( final KeyEvent event ) {
+ cut();
+ }
+
+ private void tab( final KeyEvent event ) {
+ final var range = mTextArea.selectionProperty().getValue();
+ final var sb = new StringBuilder( 1024 );
+
+ if( range.getLength() > 0 ) {
+ final var selection = mTextArea.getSelectedText();
+
+ selection.lines().forEach(
+ l -> sb.append( "\t" ).append( l ).append( NEWLINE )
+ );
+ }
+ else {
+ sb.append( "\t" );
+ }
+
+ mTextArea.replaceSelection( sb.toString() );
+ }
+
+ private void untab( final KeyEvent event ) {
+ final var range = mTextArea.selectionProperty().getValue();
+
+ if( range.getLength() > 0 ) {
+ final var selection = mTextArea.getSelectedText();
+ final var sb = new StringBuilder( selection.length() );
+
+ selection.lines().forEach(
+ l -> sb.append( l.startsWith( "\t" ) ? l.substring( 1 ) : l )
+ .append( NEWLINE )
+ );
+
+ mTextArea.replaceSelection( sb.toString() );
+ }
+ else {
+ final var p = getCaretParagraph();
+
+ if( p.startsWith( "\t" ) ) {
+ mTextArea.selectParagraph();
+ mTextArea.replaceSelection( p.substring( 1 ) );
+ }
+ }
+ }
+
+ /**
+ * Observers may listen for changes to the property returned from this method
+ * to receive notifications when either the text or caret have changed. This
+ * should not be used to track whether the text has been modified.
+ */
+ public void addDirtyListener( ChangeListener<Boolean> listener ) {
+ mDirty.addListener( listener );
+ }
+
+ /**
+ * Surrounds the selected text or word under the caret in Markdown markup.
+ *
+ * @param token The beginning and ending token for enclosing the text.
+ */
+ private void enwrap( final String token ) {
+ enwrap( token, token );
+ }
+
+ /**
+ * Surrounds the selected text or word under the caret in Markdown markup.
+ *
+ * @param began The beginning token for enclosing the text.
+ * @param ended The ending token for enclosing the text.
+ */
+ private void enwrap( final String began, String ended ) {
+ // Ensure selected text takes precedence over the word at caret position.
+ final var selected = mTextArea.selectionProperty().getValue();
+ final var range = selected.getLength() == 0
+ ? getCaretWord()
+ : selected;
+ String text = mTextArea.getText( range );
+
+ int length = range.getLength();
+ text = trimStart( text );
+ final int beganIndex = range.getStart() + length - text.length();
+
+ length = text.length();
+ text = trimEnd( text );
+ final int endedIndex = range.getEnd() - (length - text.length());
+
+ mTextArea.replaceText( beganIndex, endedIndex, began + text + ended );
+ }
+
+ /**
+ * Inserts the given block-level markup at the current caret position
+ * within the document. This will prepend two blank lines to ensure that
+ * the block element begins at the start of a new line.
+ *
+ * @param markup The text to insert at the caret.
+ */
+ private void block( final String markup ) {
+ final int pos = mTextArea.getCaretPosition();
+ mTextArea.insertText( pos, format( "%n%n%s", markup ) );
+ }
+
+ /**
+ * Returns the caret position within the current paragraph.
+ *
+ * @return A value from 0 to the length of the current paragraph.
+ */
+ private int getCaretColumn() {
+ return mTextArea.getCaretColumn();
+ }
+
+ @Override
+ public IndexRange getCaretWord() {
+ final var paragraph = getCaretParagraph()
+ .replaceAll( "---", " " )
+ .replaceAll( "--", " " )
+ .replaceAll( "[\\[\\]{}()]", " " );
+ final var length = paragraph.length();
+ final var column = getCaretColumn();
+
+ var began = column;
+ var ended = column;
+
+ while( began > 0 && !isWhitespace( paragraph.charAt( began - 1 ) ) ) {
+ began--;
+ }
+
+ while( ended < length && !isWhitespace( paragraph.charAt( ended ) ) ) {
+ ended++;
+ }
+
+ final var iterator = BreakIterator.getWordInstance();
+ iterator.setText( paragraph );
+
+ while( began < length && iterator.isBoundary( began + 1 ) ) {
+ began++;
+ }
+
+ while( ended > 0 && iterator.isBoundary( ended - 1 ) ) {
+ ended--;
+ }
+
+ final var offset = getCaretDocumentOffset( column );
+
+ return IndexRange.normalize( began + offset, ended + offset );
+ }
+
+ private int getCaretDocumentOffset( final int column ) {
+ return mTextArea.getCaretPosition() - column;
+ }
+
+ /**
+ * Returns the index of the paragraph where the caret resides.
+ *
+ * @return A number greater than or equal to 0.
+ */
+ private int getCurrentParagraph() {
+ return mTextArea.getCurrentParagraph();
+ }
+
+ /**
+ * Returns the text for the paragraph that contains the caret.
+ *
+ * @return A non-null string, possibly empty.
+ */
+ private String getCaretParagraph() {
+ return getText( getCurrentParagraph() );
+ }
+
+ @Override
+ public String getText( final int paragraph ) {
+ return mTextArea.getText( paragraph );
+ }
+
+ @Override
+ public String getText( final IndexRange indexes )
+ throws IndexOutOfBoundsException {
+ return mTextArea.getText( indexes.getStart(), indexes.getEnd() );
+ }
+
+ @Override
+ public void replaceText( final IndexRange indexes, final String s ) {
+ mTextArea.replaceText( indexes, s );
+ }
+
+ private UndoManager<?> getUndoManager() {
+ return mTextArea.getUndoManager();
+ }
+
+ /**
+ * Returns the path to a {@link Locale}-specific stylesheet.
+ *
+ * @return A non-null string to inject into the HTML document head.
+ */
+ private static String getStylesheetPath( final Locale locale ) {
+ return MessageFormat.format(
+ sSettings.getSetting( STYLESHEET_MARKDOWN_LOCALE, "" ),
+ locale.getLanguage(),
+ locale.getScript(),
+ locale.getCountry()
+ );
+ }
+
+ private Locale getLocale() {
+ return localeProperty().toLocale();
+ }
+
+ private LocaleProperty localeProperty() {
+ return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
+ }
+
+ /**
+ * Sets the font family name and font size at the same time. When the
+ * workspace is loaded, the default font values are changed, which results
+ * in this method being called.
+ *
+ * @param area Change the font settings for this text area.
+ * @param name New font family name to apply.
+ * @param points New font size to apply (in points, not pixels).
+ */
+ private void setFont(
+ final StyleClassedTextArea area, final String name, final double points ) {
+ runLater( () -> area.setStyle(
+ format(
+ "-fx-font-family:'%s';-fx-font-size:%spx;", name, toPixels( points )
+ )
+ ) );
+ }
+
+ private String getFontName() {
+ return fontNameProperty().get();
+ }
+
+ private StringProperty fontNameProperty() {
+ return mWorkspace.stringProperty( KEY_UI_FONT_EDITOR_NAME );
+ }
+
+ private double getFontSize() {
+ return fontSizeProperty().get();
+ }
+
+ private DoubleProperty fontSizeProperty() {
+ return mWorkspace.doubleProperty( KEY_UI_FONT_EDITOR_SIZE );
+ }
+
+ /**
+ * Answers whether the given resource is of compatible {@link MediaType}s.
+ *
+ * @param mediaType The {@link MediaType} to compare.
+ * @return {@code true} if the given {@link MediaType} is suitable for
+ * editing with this type of editor.
+ */
+ @Override
+ public boolean supports( final MediaType mediaType ) {
+ return isMediaType( mediaType ) ||
+ mediaType == TEXT_MARKDOWN ||
+ mediaType == TEXT_R_MARKDOWN;
}
}
src/main/java/com/keenwrite/io/SysFile.java
import static com.keenwrite.io.WindowsRegistry.pathsWindows;
import static com.keenwrite.util.DataTypeConverter.toHex;
+import static com.keenwrite.util.SystemUtils.IS_OS_WINDOWS;
import static java.lang.System.getenv;
import static java.nio.file.Files.isExecutable;
import static java.util.regex.Pattern.quote;
-import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
/**
src/main/java/com/keenwrite/io/UserDataDir.java
import static com.keenwrite.io.SysFile.toFile;
+import static com.keenwrite.util.SystemUtils.*;
import static java.lang.System.getProperty;
import static java.lang.System.getenv;
-import static org.apache.commons.lang3.SystemUtils.*;
/**
src/main/java/com/keenwrite/processors/text/StringUtilsReplacer.java
package com.keenwrite.processors.text;
-import org.apache.commons.lang3.StringUtils;
-
import java.util.Map;
-import static org.apache.commons.lang3.StringUtils.replaceEach;
+import static com.keenwrite.util.Strings.replaceEach;
/**
- * Replaces text using a brute-force
- * {@link StringUtils#replaceEach(String, String[], String[])}} method.
+ * Replaces text using a brute-force replacement method.
*/
public class StringUtilsReplacer extends AbstractTextReplacer {
/**
* Default (empty) constructor.
*/
- protected StringUtilsReplacer() { }
+ protected StringUtilsReplacer() {}
@Override
src/main/java/com/keenwrite/typesetting/containerization/Podman.java
-/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ */
package com.keenwrite.typesetting.containerization;
import static com.keenwrite.events.StatusEvent.clue;
import static com.keenwrite.io.SysFile.toFile;
+import static com.keenwrite.util.SystemUtils.IS_OS_WINDOWS;
import static java.lang.String.format;
import static java.lang.String.join;
import static java.lang.System.arraycopy;
import static java.util.Arrays.copyOf;
-import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
/**
src/main/java/com/keenwrite/typesetting/installer/TypesetterInstaller.java
import static com.keenwrite.Messages.get;
import static com.keenwrite.events.Bus.register;
-import static org.apache.commons.lang3.SystemUtils.*;
+import static com.keenwrite.util.SystemUtils.*;
/**
src/main/java/com/keenwrite/typesetting/installer/panes/ManagerOutputPane.java
+/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ */
package com.keenwrite.typesetting.installer.panes;
import com.keenwrite.io.CommandNotFoundException;
import com.keenwrite.typesetting.containerization.ContainerManager;
import com.keenwrite.typesetting.containerization.StreamProcessor;
+import com.keenwrite.util.FailableBiConsumer;
+import javafx.collections.ObservableMap;
import javafx.concurrent.Task;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
-import org.apache.commons.lang3.function.FailableBiConsumer;
import org.controlsfx.dialog.Wizard;
*/
public abstract class ManagerOutputPane extends InstallerPane {
- private final String PROP_EXECUTOR = getClass().getCanonicalName();
+ private final static String PROP_EXECUTOR =
+ ManagerOutputPane.class.getCanonicalName();
private final String mCorrectKey;
return;
}
-
- final Task<Void> task = createTask( () -> {
- mFc.accept(
- mContainer,
- input -> gobble( input, line -> append( mTextArea, line ) )
- );
- properties.remove( thread );
- return null;
- } );
-
- task.setOnSucceeded( event -> {
- append( mTextArea, get( mCorrectKey ) );
- properties.remove( thread );
- disableNext( false );
- } );
- task.setOnFailed( event -> append( mTextArea, get( mMissingKey ) ) );
- task.setOnCancelled( event -> append( mTextArea, get( mMissingKey ) ) );
+ final Task<Void> task = createTask( properties, thread );
final var executor = createThread( task );
+
properties.put( PROP_EXECUTOR, executor );
executor.start();
} catch( final Exception e ) {
throw new RuntimeException( e );
}
+ }
+
+ private Task<Void> createTask(
+ final ObservableMap<Object, Object> properties,
+ final Object thread ) {
+ final Task<Void> task = createTask( () -> {
+ mFc.accept(
+ mContainer,
+ input -> gobble( input, line -> append( mTextArea, line ) )
+ );
+ properties.remove( thread );
+ return null;
+ } );
+
+ task.setOnSucceeded( _ -> {
+ append( mTextArea, get( mCorrectKey ) );
+ properties.remove( thread );
+ disableNext( false );
+ } );
+ task.setOnFailed( _ -> append( mTextArea, get( mMissingKey ) ) );
+ task.setOnCancelled( _ -> append( mTextArea, get( mMissingKey ) ) );
+ return task;
}
}
src/main/java/com/keenwrite/typesetting/installer/panes/UnixManagerInstallPane.java
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
-import org.jetbrains.annotations.NotNull;
import static com.keenwrite.Messages.get;
import static com.keenwrite.Messages.getInt;
+import static com.keenwrite.util.SystemUtils.IS_OS_MAC;
import static java.lang.String.format;
-import static org.apache.commons.lang3.SystemUtils.IS_OS_MAC;
public final class UnixManagerInstallPane extends InstallerPane {
final var node = super.createButtonBar();
final var layout = new BorderPane();
- final var copyButton = button( PREFIX + ".copy.began" );
+ final var copyButton = button( STR."\{PREFIX}.copy.began" );
// Change the label to indicate clipboard is updated.
- copyButton.setOnAction( event -> {
+ copyButton.setOnAction( _ -> {
SystemClipboard.write( mCommands.getText() );
- copyButton.setText( get( PREFIX + ".copy.ended" ) );
+ copyButton.setText( get( STR."\{PREFIX}.copy.ended" ) );
} );
@Override
protected String getHeaderKey() {
- return PREFIX + ".header";
+ return STR."\{PREFIX}.header";
}
private record UnixOsCommand( String name, String command )
implements Comparable<UnixOsCommand> {
@Override
- public int compareTo(
- final @NotNull UnixOsCommand other ) {
+ public int compareTo( final UnixOsCommand other ) {
return toString().compareToIgnoreCase( other.toString() );
}
final var comboBox = new ComboBox<UnixOsCommand>();
final var items = comboBox.getItems();
- final var prefix = PREFIX + ".command";
- final var distros = getInt( prefix + ".distros", 14 );
+ final var prefix = STR."\{PREFIX}.command";
+ final var distros = getInt( STR."\{prefix}.distros", 14 );
for( int i = 1; i <= distros; i++ ) {
final var suffix = format( ".%02d", i );
- final var name = get( prefix + ".os.name" + suffix );
- final var command = get( prefix + ".os.text" + suffix );
+ final var name = get( STR."\{prefix}.os.name\{suffix}" );
+ final var command = get( STR."\{prefix}.os.text\{suffix}" );
items.add( new UnixOsCommand( name, command ) );
src/main/java/com/keenwrite/ui/dialogs/ExportDialog.java
import static com.keenwrite.io.SysFile.toFile;
import static com.keenwrite.util.FileWalker.walk;
+import static com.keenwrite.util.Strings.abbreviate;
import static java.lang.Math.max;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javafx.application.Platform.runLater;
import static javafx.geometry.Pos.CENTER;
import static javafx.scene.control.ButtonType.OK;
-import static org.apache.commons.lang3.StringUtils.abbreviate;
/**
src/main/java/com/keenwrite/util/CyclicIterator.java
-/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
+/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved.
+ *
+ * SPDX-License-Identifier: MIT
+ */
package com.keenwrite.util;
src/main/java/com/keenwrite/util/FailableBiConsumer.java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.keenwrite.util;
+
+import java.util.function.BiConsumer;
+
+/**
+ * A functional interface like {@link BiConsumer} that declares a {@link Throwable}.
+ *
+ * @param <T> Consumed type 1.
+ * @param <U> Consumed type 2.
+ * @param <E> The kind of thrown exception or error.
+ */
+@FunctionalInterface
+public interface FailableBiConsumer<T, U, E extends Throwable> {
+
+ /**
+ * Accepts the given arguments.
+ *
+ * @param t the first parameter for the consumable to accept
+ * @param u the second parameter for the consumable to accept
+ * @throws E Thrown when the consumer fails.
+ */
+ void accept(T t, U u) throws E;
+}
src/main/java/com/keenwrite/util/Strings.java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.keenwrite.util;
+
+import java.lang.reflect.Array;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static java.lang.Character.isWhitespace;
+import static java.lang.String.format;
+
+/**
+ * Java doesn't allow adding behaviour to its {@link String} class, so these
+ * functions have no alternative home. They are duplicated here to eliminate
+ * the dependency on an Apache library. Extracting the methods that only
+ * the application uses may have some small performance gains, as well,
+ * because numerous if clauses have been removed and other code simplified.
+ */
+public class Strings {
+ /**
+ * The empty String {@code ""}.
+ */
+ private static final String EMPTY = "";
+
+ /**
+ * Abbreviates a String using ellipses. This will turn
+ * "Now is the time for all good men" into "Now is the time for..."
+ *
+ * @param str the String to check, may be {@code null}.
+ * @param width maximum length of result String, must be at least 4.
+ * @return abbreviated String, {@code null} if {@code null} String input.
+ * @throws IllegalArgumentException if the width is too small.
+ */
+ public static String abbreviate( final String str, final int width ) {
+ return abbreviate( str, "...", 0, width );
+ }
+
+ /**
+ * Abbreviates a String using another given String as replacement marker.
+ * This will turn"Now is the time for all good men" into "Now is the time
+ * for..." if "..." was defined as the replacement marker.
+ *
+ * @param str the String to check, may be {@code null}.
+ * @param abbrMarker the String used as replacement marker.
+ * @param width maximum length of result String, must be at least
+ * {@code abbrMarker.length + 1}.
+ * @return abbreviated String, {@code null} if {@code null} String input.
+ * @throws IllegalArgumentException if the width is too small.
+ */
+ public static String abbreviate(
+ final String str,
+ final String abbrMarker,
+ final int width ) {
+ return abbreviate( str, abbrMarker, 0, width );
+ }
+
+ /**
+ * Abbreviates a String using a given replacement marker. This will turn
+ * "Now is the time for all good men" into "...is the time for..." if "..."
+ * was defined as the replacement marker.
+ *
+ * @param str the String to check, may be {@code null}.
+ * @param abbrMarker the String used as replacement marker.
+ * @param offset left edge of source String.
+ * @param width maximum length of result String, must be at least 4.
+ * @return abbreviated String, {@code null} if {@code null} String input.
+ * @throws IllegalArgumentException if the width is too small.
+ */
+ public static String abbreviate(
+ final String str,
+ final String abbrMarker,
+ int offset,
+ final int width ) {
+ if( !isEmpty( str ) && EMPTY.equals( abbrMarker ) && width > 0 ) {
+ return substring( str, width );
+ }
+
+ if( isAnyEmpty( str, abbrMarker ) ) {
+ return str;
+ }
+
+ final int abbrMarkerLen = abbrMarker.length();
+ final int minAbbrWidth = abbrMarkerLen + 1;
+ final int minAbbrWidthOffset = abbrMarkerLen + abbrMarkerLen + 1;
+
+ if( width < minAbbrWidth ) {
+ final String msg = format( "Min abbreviation width: %d", minAbbrWidth );
+ throw new IllegalArgumentException( msg );
+ }
+
+ final int strLen = str.length();
+
+ if( strLen <= width ) {
+ return str;
+ }
+
+ if( offset > strLen ) {
+ offset = strLen;
+ }
+
+ if( strLen - offset < width - abbrMarkerLen ) {
+ offset = strLen - (width - abbrMarkerLen);
+ }
+
+ if( offset <= abbrMarkerLen + 1 ) {
+ return str.substring( 0, width - abbrMarkerLen ) + abbrMarker;
+ }
+
+ if( width < minAbbrWidthOffset ) {
+ final String msg = format(
+ "Min abbreviation width with offset: %d",
+ minAbbrWidthOffset
+ );
+ throw new IllegalArgumentException( msg );
+ }
+
+ if( offset + width - abbrMarkerLen < strLen ) {
+ return abbrMarker + abbreviate(
+ str.substring( offset ),
+ abbrMarker,
+ width - abbrMarkerLen
+ );
+ }
+
+ return abbrMarker + str.substring( strLen - (width - abbrMarkerLen) );
+ }
+
+ /**
+ * Strips whitespace characters from the end of a String.
+ *
+ * <p>A {@code null} input String returns {@code null}.
+ * An empty string ("") input returns the empty string.</p>
+ *
+ * @param str the String to remove characters from, may be {@code null}.
+ * @return the stripped String, {@code null} if {@code null} input.
+ */
+ public static String trimEnd( final String str ) {
+ int end = length( str );
+
+ if( end == 0 ) {
+ return str;
+ }
+
+ while( end != 0 && isWhitespace( str.charAt( end - 1 ) ) ) {
+ end--;
+ }
+
+ return str.substring( 0, end );
+ }
+
+ /**
+ * Strips whitespace characters from the start of a String.
+ *
+ * <p>A {@code null} input returns {@code null}.
+ * An empty string ("") input returns the empty string.</p>
+ *
+ * @param str the String to remove characters from, may be {@code null}.
+ * @return the stripped String, {@code null} if {@code null} input.
+ */
+ public static String trimStart( final String str ) {
+ final int strLen = length( str );
+
+ if( strLen == 0 ) {
+ return str;
+ }
+
+ int start = 0;
+
+ while( start != strLen && isWhitespace( str.charAt( start ) ) ) {
+ start++;
+ }
+
+ return str.substring( start );
+ }
+
+ /**
+ * Replaces all occurrences of Strings within another String.
+ *
+ * @param text the haystack, no-op if {@code null}.
+ * @param searchList the needles, no-op if {@code null}.
+ * @param replacementList the new needles, no-op if {@code null}.
+ * @return the text with any replacements processed, {@code null} if
+ * {@code null} String input.
+ * @throws IllegalArgumentException if the lengths of the arrays are not
+ * the same ({@code null} is ok, and/or
+ * size 0).
+ */
+ public static String replaceEach( final String text,
+ final String[] searchList,
+ final String[] replacementList ) {
+ return replaceEach( text, searchList, replacementList, 0 );
+ }
+
+ /**
+ * Replace all occurrences of Strings within another String.
+ *
+ * @param text the haystack, no-op if {@code null}.
+ * @param searchList the needles, no-op if {@code null}.
+ * @param replacementList the new needles, no-op if {@code null}.
+ * @param timeToLive if less than 0 then there is a circular reference
+ * and endless loop
+ * @return the text with any replacements processed, {@code null} if
+ * {@code null} String input.
+ * @throws IllegalStateException if the search is repeating and there is
+ * an endless loop due to outputs of one
+ * being inputs to another
+ * @throws IllegalArgumentException if the lengths of the arrays are not
+ * the same ({@code null} is ok, and/or
+ * size 0)
+ */
+ private static String replaceEach(
+ final String text,
+ final String[] searchList,
+ final String[] replacementList,
+ final int timeToLive
+ ) {
+ // If in a recursive call, this shouldn't be less than zero.
+ if( timeToLive < 0 ) {
+ final Set<String> searchSet =
+ new HashSet<>( Arrays.asList( searchList ) );
+ final Set<String> replacementSet = new HashSet<>( Arrays.asList(
+ replacementList ) );
+ searchSet.retainAll( replacementSet );
+ if( !searchSet.isEmpty() ) {
+ throw new IllegalStateException(
+ "Aborting to protect against StackOverflowError - " +
+ "output of one loop is the input of another" );
+ }
+ }
+
+ if( isEmpty( text ) ||
+ isEmpty( searchList ) ||
+ isEmpty( replacementList ) ||
+ isNotEmpty( searchList ) &&
+ timeToLive == -1 ) {
+ return text;
+ }
+
+ final int searchLength = searchList.length;
+ final int replacementLength = replacementList.length;
+
+ // make sure lengths are ok, these need to be equal
+ if( searchLength != replacementLength ) {
+ final String msg = format(
+ "Search and Replace array lengths don't match: %d vs %d",
+ searchLength,
+ replacementLength
+ );
+ throw new IllegalArgumentException( msg );
+ }
+
+ // keep track of which still have matches
+ final boolean[] noMoreMatchesForReplIndex = new boolean[ searchLength ];
+
+ // index on index that the match was found
+ int textIndex = -1;
+ int replaceIndex = -1;
+ int tempIndex;
+
+ // index of replace array that will replace the search string found
+ // NOTE: logic duplicated below START
+ for( int i = 0; i < searchLength; i++ ) {
+ if( noMoreMatchesForReplIndex[ i ] || isEmpty( searchList[ i ] ) || replacementList[ i ] == null ) {
+ continue;
+ }
+ tempIndex = text.indexOf( searchList[ i ] );
+
+ // see if we need to keep searching for this
+ if( tempIndex == -1 ) {
+ noMoreMatchesForReplIndex[ i ] = true;
+ }
+ else if( textIndex == -1 || tempIndex < textIndex ) {
+ textIndex = tempIndex;
+ replaceIndex = i;
+ }
+ }
+ // NOTE: logic mostly below END
+
+ // no search strings found, we are done
+ if( textIndex == -1 ) {
+ return text;
+ }
+
+ int start = 0;
+
+ // Guess the result buffer size, to prevent doubling capacity.
+ final StringBuilder buf = createStringBuilder(
+ text, searchList, replacementList
+ );
+
+ while( textIndex != -1 ) {
+ for( int i = start; i < textIndex; i++ ) {
+ buf.append( text.charAt( i ) );
+ }
+
+ buf.append( replacementList[ replaceIndex ] );
+
+ start = textIndex + searchList[ replaceIndex ].length();
+
+ textIndex = -1;
+ replaceIndex = -1;
+
+ // find the next earliest match
+ // NOTE: logic mostly duplicated above START
+ for( int i = 0; i < searchLength; i++ ) {
+ if( noMoreMatchesForReplIndex[ i ] || isEmpty( searchList[ i ] ) || replacementList[ i ] == null ) {
+ continue;
+ }
+ tempIndex = text.indexOf( searchList[ i ], start );
+
+ // see if we need to keep searching for this
+ if( tempIndex == -1 ) {
+ noMoreMatchesForReplIndex[ i ] = true;
+ }
+ else if( textIndex == -1 || tempIndex < textIndex ) {
+ textIndex = tempIndex;
+ replaceIndex = i;
+ }
+ }
+
+ // NOTE: logic duplicated above END
+ }
+
+ final int textLength = text.length();
+ for( int i = start; i < textLength; i++ ) {
+ buf.append( text.charAt( i ) );
+ }
+
+ return replaceEach(
+ buf.toString(),
+ searchList,
+ replacementList,
+ timeToLive - 1
+ );
+ }
+
+ private static StringBuilder createStringBuilder(
+ final String text,
+ final String[] searchList,
+ final String[] replacementList ) {
+ int increase = 0;
+
+ // count the replacement text elements that are larger than their
+ // corresponding text being replaced
+ for( int i = 0; i < searchList.length; i++ ) {
+ if( searchList[ i ] == null || replacementList[ i ] == null ) {
+ continue;
+ }
+ final int greater =
+ replacementList[ i ].length() - searchList[ i ].length();
+ if( greater > 0 ) {
+ increase += 3 * greater; // assume 3 matches
+ }
+ }
+
+ // have upper-bound at 20% increase, then let Java take over
+ increase = Math.min( increase, text.length() / 5 );
+
+ return new StringBuilder( text.length() + increase );
+ }
+
+ /**
+ * Gets a {@link CharSequence} length or {@code 0} if the
+ * {@link CharSequence} is {@code null}.
+ *
+ * @param cs a {@link CharSequence} or {@code null}.
+ * @return {@link CharSequence} length or {@code 0} if the
+ * {@link CharSequence} is {@code null}.
+ */
+ private static int length( final CharSequence cs ) {
+ return cs == null ? 0 : cs.length();
+ }
+
+ /**
+ * Checks if a {@link CharSequence} is empty ("") or {@code null}.
+ *
+ * @param cs the {@link CharSequence} to check, may be {@code null}.
+ * @return {@code true} if the {@link CharSequence} is empty or {@code null}.
+ */
+ public static boolean isEmpty( final CharSequence cs ) {
+ return cs == null || cs.isEmpty();
+ }
+
+ private static boolean isEmpty( final Object[] array ) {
+ return array == null || Array.getLength( array ) == 0;
+ }
+
+ private static boolean isNotEmpty( final Object[] array ) {
+ return array != null && Array.getLength( array ) > 0;
+ }
+
+ private static boolean isAnyEmpty( final CharSequence... css ) {
+ if( isNotEmpty( css ) ) {
+ for( final CharSequence cs : css ) {
+ if( isEmpty( cs ) ) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Gets a substring from the specified String avoiding exceptions.
+ *
+ * <p>A negative start position can be used to start/end {@code n}
+ * characters from the end of the String.</p>
+ *
+ * <p>The returned substring starts with the character in the {@code start}
+ * position and ends before the {@code end} position. All position counting
+ * is zero-based -- i.e., to start at the beginning of the string use
+ * {@code start = 0}. Negative start and end positions can be used to
+ * specify offsets relative to the end of the String.</p>
+ *
+ * <p>If {@code start} is not strictly to the left of {@code end}, ""
+ * is returned.</p>
+ *
+ * @param str the String to get the substring from, may be {@code null}.
+ * @param end the position to end at (exclusive), negative means
+ * count back from the end of the String by this many characters
+ * @return substring from start position to end position, {@code null} if
+ * {@code null} String input
+ */
+ private static String substring( final String str, int end ) {
+ if( str == null ) {
+ return null;
+ }
+
+ final int len = str.length();
+
+ if( end < 0 ) {
+ end = len + end;
+ }
+
+ if( end > len ) {
+ end = len;
+ }
+
+ final int start = 0;
+
+ if( start > end ) {
+ return EMPTY;
+ }
+
+ return str.substring( start, end );
+ }
+}
src/main/java/com/keenwrite/util/SystemUtils.java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.keenwrite.util;
+
+import java.util.Properties;
+
+import static com.keenwrite.util.Strings.isEmpty;
+
+/**
+ * Helpers for {@code java.lang.System}.
+ */
+public class SystemUtils {
+
+ // System property constants
+ // -----------------------------------------------------------------------
+ // These MUST be declared first. Other constants depend on this.
+
+ /**
+ * The System property name {@value}.
+ */
+ public static final String PROPERTY_OS_NAME = "os.name";
+
+ /**
+ * Gets the current value from the system properties map.
+ * <p>
+ * Returns {@code null} if the property cannot be read due to a
+ * {@link SecurityException}.
+ * </p>
+ *
+ * @return the current value from the system properties map.
+ */
+ @SuppressWarnings( "ConstantValue" )
+ private static String getOsName() {
+ assert PROPERTY_OS_NAME != null;
+ assert !PROPERTY_OS_NAME.isBlank();
+
+ try {
+ final String value = System.getProperty( PROPERTY_OS_NAME );
+
+ return isEmpty( value ) ? "" : value;
+ } catch( final SecurityException ignore ) {}
+
+ return "";
+ }
+
+ /**
+ * The Operating System name, derived from Java's system properties.
+ *
+ * <p>
+ * Defaults to empty if the runtime does not have security access to
+ * read this property or the property does not exist.
+ * </p>
+ * <p>
+ * This value is initialized when the class is loaded. If
+ * {@link System#setProperty(String, String)} or
+ * {@link System#setProperties(Properties)} is called after this
+ * class is loaded, the value will be out of sync with that System property.
+ * </p>
+ */
+ public static final String OS_NAME = getOsName();
+
+ /**
+ * Is {@code true} if this is AIX.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_AIX = osNameMatches( "AIX" );
+
+ /**
+ * Is {@code true} if this is HP-UX.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_HP_UX = osNameMatches( "HP-UX" );
+
+ /**
+ * Is {@code true} if this is Irix.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_IRIX = osNameMatches( "Irix" );
+
+ /**
+ * Is {@code true} if this is Linux.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_LINUX =
+ osNameMatches( "Linux" ) ||
+ osNameMatches( "LINUX" );
+
+ /**
+ * Is {@code true} if this is Mac.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_MAC = osNameMatches( "Mac" );
+
+ /**
+ * Is {@code true} if this is Mac.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_MAC_OSX = osNameMatches( "Mac OS X" );
+
+ /**
+ * Is {@code true} if this is FreeBSD.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_FREE_BSD = osNameMatches( "FreeBSD" );
+
+ /**
+ * Is {@code true} if this is OpenBSD.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_OPEN_BSD = osNameMatches( "OpenBSD" );
+
+ /**
+ * Is {@code true} if this is NetBSD.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_NET_BSD = osNameMatches( "NetBSD" );
+
+ /**
+ * Is {@code true} if this is Solaris.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_SOLARIS = osNameMatches( "Solaris" );
+
+ /**
+ * Is {@code true} if this is SunOS.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_SUN_OS = osNameMatches( "SunOS" );
+
+ /**
+ * Is {@code true} if this is a UNIX like system, as in any of AIX, HP-UX,
+ * Irix, Linux, MacOSX, Solaris or SUN OS.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_UNIX =
+ IS_OS_AIX ||
+ IS_OS_HP_UX ||
+ IS_OS_IRIX ||
+ IS_OS_LINUX ||
+ IS_OS_MAC_OSX ||
+ IS_OS_SOLARIS ||
+ IS_OS_SUN_OS ||
+ IS_OS_FREE_BSD ||
+ IS_OS_OPEN_BSD ||
+ IS_OS_NET_BSD;
+
+ /**
+ * The prefix String for all Windows OS.
+ */
+ private static final String OS_NAME_WINDOWS_PREFIX = "Windows";
+
+ /**
+ * Is {@code true} if this is Windows.
+ *
+ * <p>
+ * The field will return {@code false} if {@code OS_NAME} is {@code null}.
+ * </p>
+ */
+ public static final boolean IS_OS_WINDOWS =
+ osNameMatches( OS_NAME_WINDOWS_PREFIX );
+
+ /**
+ * Decides if the operating system matches.
+ * <p>
+ * This method is package private instead of private to support unit test
+ * invocation.
+ * </p>
+ *
+ * @param prefix the prefix for the expected OS name
+ * @return true if matches, or false if not or can't determine
+ */
+ private static boolean osNameMatches( final String prefix ) {
+ return OS_NAME.startsWith( prefix );
+ }
+}

Removes dependency on Apache commons lang3

Author DaveJarvis <email>
Date 2023-12-22 14:29:55 GMT-0800
Commit 750c4f7b6913422485b45376b3f521bd72d3bf1d
Parent 3390003
Delta 1535 lines added, 789 lines removed, 746-line increase