package com.scrivenvar.editors.markdown;
import com.scrivenvar.dialogs.ImageDialog;
import com.scrivenvar.dialogs.LinkDialog;
import com.scrivenvar.editors.EditorPane;
import com.scrivenvar.processors.markdown.BlockExtension;
import com.scrivenvar.processors.markdown.MarkdownProcessor;
import com.vladsch.flexmark.ast.Link;
import com.vladsch.flexmark.html.renderer.AttributablePart;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.html.MutableAttributes;
import javafx.scene.control.Dialog;
import javafx.scene.control.IndexRange;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Window;
import org.fxmisc.richtext.StyleClassedTextArea;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
import static com.scrivenvar.util.Utils.ltrim;
import static com.scrivenvar.util.Utils.rtrim;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
public class MarkdownEditorPane extends EditorPane {
private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
"(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
private static final Pattern PATTERN_NEW_LINE = Pattern.compile(
"^>|(((#+)|([*+\\-])|([1-9]\\.))\\s+).+" );
public MarkdownEditorPane() {
initEditor();
}
private void initEditor() {
final StyleClassedTextArea textArea = getEditor();
textArea.setWrapText( true );
textArea.getStyleClass().add( "markdown-editor" );
textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut );
}
public void insertLink() {
insertObject( createLinkDialog() );
}
public void insertImage() {
insertObject( createImageDialog() );
}
public int approximateParagraphId( final int paraIndex ) {
final StyleClassedTextArea editor = getEditor();
final List<String> lines = new ArrayList<>( 4096 );
int i = 0;
String prevText = "";
boolean withinFencedBlock = false;
boolean withinCodeBlock = false;
for( final var p : editor.getParagraphs() ) {
if( i > paraIndex ) {
break;
}
final String text = p.getText().replace( '>', ' ' );
if( text.startsWith( "```" ) ) {
if( withinFencedBlock = !withinFencedBlock ) {
lines.add( text );
}
}
if( !withinFencedBlock ) {
final boolean foundCodeBlock = text.startsWith( " " );
if( foundCodeBlock && !withinCodeBlock ) {
lines.add( text );
withinCodeBlock = true;
}
else if( !foundCodeBlock ) {
withinCodeBlock = false;
}
}
if( !withinFencedBlock && !withinCodeBlock &&
((!text.isBlank() && prevText.isBlank()) ||
PATTERN_NEW_LINE.matcher( text ).matches()) ) {
lines.add( text );
}
prevText = text;
i++;
}
return Math.max( lines.size() - 1, 0 );
}
public int getCurrentParagraphIndex() {
return getEditor().getCurrentParagraph();
}
public void surroundSelection( final String leading, final String trailing ) {
surroundSelection( leading, trailing, null );
}
public void surroundSelection(
String leading, String trailing, final String hint ) {
final StyleClassedTextArea textArea = getEditor();
final IndexRange selection = textArea.getSelection();
int start = selection.getStart();
int end = selection.getEnd();
final String selectedText = textArea.getSelectedText();
String trimmedText = selectedText.trim();
if( trimmedText.length() < selectedText.length() ) {
start += selectedText.indexOf( trimmedText );
end = start + trimmedText.length();
}
if( start == 0 ) {
leading = ltrim( leading );
}
if( end == textArea.getLength() ) {
trailing = rtrim( trailing );
}
if( leading.startsWith( "\n" ) ) {
for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
break;
}
leading = leading.substring( 1 );
}
}
final boolean trailingIsEmpty = trailing.isEmpty();
String str = trailingIsEmpty ? leading : trailing;
if( str.endsWith( "\n" ) ) {
final int length = textArea.getLength();
for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
break;
}
str = str.substring( 0, str.length() - 1 );
}
if( trailingIsEmpty ) {
leading = str;
}
else {
trailing = str;
}
}
int selStart = start + leading.length();
int selEnd = end + leading.length();
if( hint != null && trimmedText.isEmpty() ) {
trimmedText = hint;
selEnd = selStart + hint.length();
}
getUndoManager().preventMerge();
textArea.replaceText( start, end, leading + trimmedText + trailing );
textArea.selectRange( selStart, selEnd );
}
private void enterPressed( final KeyEvent e ) {
final StyleClassedTextArea textArea = getEditor();
final String currentLine =
textArea.getText( textArea.getCurrentParagraph() );
final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
String newText = "\n";
if( matcher.matches() ) {
if( !matcher.group( 2 ).isEmpty() ) {
newText = newText.concat( matcher.group( 1 ) );
}
else {
final int caretPosition = textArea.getCaretPosition();
textArea.selectRange( caretPosition - currentLine.length(),
caretPosition );
}
}
textArea.replaceSelection( newText );
textArea.requestFollowCaret();
}
private void cut( final KeyEvent event ) {
super.cut();
}
private HyperlinkModel getHyperlink() {
final StyleClassedTextArea textArea = getEditor();
final String selectedText = textArea.getSelectedText();
final MarkdownProcessor mp = new MarkdownProcessor( null );
final int p = textArea.getCurrentParagraph();
final String paragraph = textArea.getText( p );
final Node node = mp.toNode( paragraph );
final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
final Link link = visitor.process( node );
if( link != null ) {
textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
}
return createHyperlinkModel(
link, selectedText, "https://localhost"
);
}
@SuppressWarnings("SameParameterValue")
private HyperlinkModel createHyperlinkModel(
final Link link, final String selection, final String url ) {
return link == null
? new HyperlinkModel( selection, url )
: new HyperlinkModel( link );
}
private Path getParentPath() {
final Path path = getPath();
return (path != null) ? path.getParent() : null;
}
private Dialog<String> createLinkDialog() {
return new LinkDialog( getWindow(), getHyperlink() );
}
private Dialog<String> createImageDialog() {
return new ImageDialog( getWindow(), getParentPath() );
}
private void insertObject( final Dialog<String> dialog ) {
dialog.showAndWait().ifPresent(
result -> getEditor().replaceSelection( result )
);
}
private Window getWindow() {
return getScrollPane().getScene().getWindow();
}
}