0

I'm making a simple markdown text editor with Swing which can render markdown formatting live in its main JTextPane. That is, markdown-formatted text like *hello* is displayed in italics once detected, however is saved in plain text.

To this end I came up with a regex to pick up markdown tokens (let's just use *italics* as an example for now), and every few seconds my code searches the text of the JTextPane and uses JTextPane#setCharacterAttributes to change the formatting of areas in question.

// init
PLAIN = Document.addStyle("plain", null);
StyleConstants.setFontSize(PLAIN, 12);

ITALIC = Document.addStyle("italic", null);
StyleConstants.setItalic(ITALIC, true);
...
// every few seconds
// remove all formatting
Document.setCharacterAttributes(0, Document.getLength(), PLAIN, true);

// italicize parts that the regex matches
m = Pattern.compile("\\*([^\\n*]+)\\*").matcher(temp);
while (m.find()) {
    Document.setCharacterAttributes(m.start(), m.group().length(), ITALIC, false);
}

The problem is the 'liveness' - after some time, the JTextPane starts wrapping by characters instead of words, and at times loses word wrapping altogether and simply displays unwrapped lines.

Is there any way I can fix this/extend the JTextPane to fix it, or is the JTextPane simply not suited to such live updating? I googled for a really long time but couldn't find anything; I'm just not sure what to search for.

package test;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.lang.reflect.Field;
import java.util.logging.*;
import java.util.regex.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

public class Test {

    private JFrame frame = new JFrame();
    private JTextPane jtp;
    private StyledDocument doc;

    // NEW LINES
    private Timer T;
    private boolean update = true;
    MarkdownRenderer m;
    // END OF NEW LINES

    public Test() {
        jtp = new JTextPane();
        jtp.setEditorKit(new MyStyledEditorKit());
        jtp.setText("\ntype some text in the above empty line and check the wrapping behavior");
        doc = jtp.getStyledDocument();
        // NEW LINES
        m = new MarkdownRenderer(jtp);
        Timer T = new Timer(2000, new java.awt.event.ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (update) {
                    update = false;
                    m.render();
                }
            }
        });
        T.start();
        // END OF NEW LINES
        doc.addDocumentListener(new DocumentListener() {

            private boolean doUpdate = true;
            public void insertUpdate(DocumentEvent e) {
                insert();
            }

            public void removeUpdate(DocumentEvent e) {
                insert();
            }

            public void changedUpdate(DocumentEvent e) {
//                triggers every time formatting is changed
//                 insert();
            }

            public void insert() {
                SwingUtilities.invokeLater(new Runnable() {

                    public void run() {
                        Style defaultStyle = jtp.getStyle(StyleContext.DEFAULT_STYLE);
                        doc.setCharacterAttributes(0, doc.getLength(), defaultStyle, false);
                        update = true;
                    }
                });
            }
        });
        JScrollPane scroll = new JScrollPane(jtp);
        scroll.setPreferredSize(new Dimension(200, 200));
        frame.add(scroll);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                Test bugWrapJava7 = new Test();
            }
        });
    }
}

class MyStyledEditorKit extends StyledEditorKit {
    private MyFactory factory;

    public ViewFactory getViewFactory() {
        if (factory == null) {
            factory = new MyFactory();
        }
        return factory;
    }
}

class MyFactory implements ViewFactory {
    public View create(Element elem) {
        String kind = elem.getName();
        if (kind != null) {
            if (kind.equals(AbstractDocument.ContentElementName)) {
                return new MyLabelView(elem);
            } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                return new ParagraphView(elem);
            } else if (kind.equals(AbstractDocument.SectionElementName)) {
                return new BoxView(elem, View.Y_AXIS);
            } else if (kind.equals(StyleConstants.ComponentElementName)) {
                return new ComponentView(elem);
            } else if (kind.equals(StyleConstants.IconElementName)) {
                return new IconView(elem);
            }
        }

        // default to text display
        return new LabelView(elem);
    }
}

class MyLabelView extends LabelView {
    public MyLabelView(Element elem) {
        super(elem);
    }
    public View breakView(int axis, int p0, float pos, float len) {
        if (axis == View.X_AXIS) {
            resetBreakSpots();
        }
        return super.breakView(axis, p0, pos, len);
    }

    private void resetBreakSpots() {
        try {
            // HACK the breakSpots private fields
            Field f=GlyphView.class.getDeclaredField("breakSpots");
            f.setAccessible(true);
            f.set(this, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}


class MarkdownRenderer {    
    private static final MutableAttributeSet PLAIN = new SimpleAttributeSet();
    private static final MutableAttributeSet BOLD = new SimpleAttributeSet();
    private static final MutableAttributeSet ITALIC = new SimpleAttributeSet();
    private static final MutableAttributeSet UNDERLINE = new SimpleAttributeSet();

    private StyledDocument Document = null;

    public MarkdownRenderer(JTextPane editor) {
        Document = (StyledDocument) editor.getDocument();

        StyleConstants.setBold(BOLD, true);
        StyleConstants.setItalic(ITALIC, true);
        StyleConstants.setUnderline(UNDERLINE, true);
    }

    void render() {
        String s = "";
        try {
            s = Document.getText(0, Document.getLength());
        } catch (BadLocationException ex) {
            Logger.getLogger(MarkdownRenderer.class.getName()).log(Level.SEVERE, null, ex);
        }
//        Document.setCharacterAttributes(0, Document.getLength(), PLAIN, true);

        String temp = s.replaceAll("\\*\\*([^\\n*]+)\\*\\*", "|`$1`|"); // can also use lazy quantifier: (.+?)

        Matcher m = Pattern.compile("\\|`.+?`\\|").matcher(temp);
        while (m.find()) {
            Document.setCharacterAttributes(m.start(), m.group().length(), BOLD, false);
        }
        m = Pattern.compile("\\*([^\\n*]+)\\*").matcher(temp);
        while (m.find()) {
            Document.setCharacterAttributes(m.start(), m.group().length(), ITALIC, false);
        }
        m = Pattern.compile("_+([^\\n*]+)_+").matcher(temp);
        while (m.find()) {
            Document.setCharacterAttributes(m.start(), m.group().length(), UNDERLINE, false);
        }
    }
}
Community
  • 1
  • 1
user1953221
  • 419
  • 1
  • 3
  • 9
  • So let me make sure that I fully understand you. You are using polling to show the mark-downs as opposed to showing text in two ways similar to stackoveflow -- in an editing format where asterisks show up as asterisks and in a display format where they are changed to italics. – Hovercraft Full Of Eels Jan 12 '13 at 13:46
  • JTextPane may be well suited for that (this can be proved by the fact that it supports HTML) but it can come down to implement your own EditorKit (parsers, view factory, etc...) which can be quite cumbersome. Any reason for not using HTML? (Or if your markdown is quite simple, you could use HTML as a display format and convert HTML to your markdown back and forward) – Guillaume Polet Jan 12 '13 at 13:52
  • @Hovercraft Yup. The text editor also has an export function which outputs pure HTML. It's just that during the editing process I thought live feedback on markdown formatting would be cool, if it could be easily implemented. – user1953221 Jan 12 '13 at 13:58
  • @Guillaume Polet By "HTML as a display format", do you mean searching the JTextPane as I do currently, but then using HTML to reflect the changes as opposed to with setCharacterAttributes? Would the JTextPane still be editable in plain text after? – user1953221 Jan 12 '13 at 14:02
  • I am not sure it would fit your needs but I would "convert" your input text containing markdown to HTML before setting it on the JTextPane, and on the other hand, I would convert HTML back to your format before retrieving the text (using HTMLParser, this is very simple to perform). As a side note to this, using regexp to parse a language is not a good choice. – Guillaume Polet Jan 12 '13 at 14:05
  • 3
    That's issue with letter wrap please check http://stackoverflow.com/questions/11000220/strange-text-wrapping-with-styled-text-in-jtextpane-with-java-7/14230668#comment19822995_14230668 – StanislavL Jan 12 '13 at 14:13
  • @Guillaume Polet My main requirement is that the program remain a text editor (can edit/save/copy/paste plain text), which was why my initial approach did not use HTML. Is regex bad because it's too low-level? – user1953221 Jan 13 '13 at 07:08
  • @StanislavL Wow, interesting... Nice find! This solves my problem, apart from some minor scrolling issues. Are these bugs ever officially fixed? – user1953221 Jan 13 '13 at 07:13
  • @user1953221 don't know about official fix. I am not sure it was posted as a bug. – StanislavL Jan 13 '13 at 08:42
  • @StanislavL Hmm, I added a self-contained example to my original post, adapted from BugWrapJava7 in the post you linked. As you can see, there are no problems when dealing with unformatted text, but as soon as something is formatted with `**bold**` or `*italics*` in markdown, the text stops wrapping when typing or pasting long lines into the pane. What am I doing wrong? – user1953221 Jan 13 '13 at 09:02
  • @user1953221 See explanation here: http://kore-nordmann.de/blog/do_NOT_parse_using_regexp.html Regexp are useful to match words and patterns, not structures. – Guillaume Polet Jan 13 '13 at 10:37

0 Answers0