com.vladsch.idea.multimarkdown.editor.MultiMarkdownPreviewEditor.java Source code

Java tutorial

Introduction

Here is the source code for com.vladsch.idea.multimarkdown.editor.MultiMarkdownPreviewEditor.java

Source

/*
 * Copyright (c) 2011-2014 Julien Nicoulaud <julien.nicoulaud@gmail.com>
 * Copyright (c) 2015-2015 Vladimir Schneider <vladimir.schneider@gmail.com>
 *
 * 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.vladsch.idea.multimarkdown.editor;

import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
import com.intellij.ide.structureView.StructureViewBuilder;
import com.intellij.lang.Language;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.components.JBScrollPane;
import com.vladsch.idea.multimarkdown.MultiMarkdownBundle;
import com.vladsch.idea.multimarkdown.MultiMarkdownPlugin;
import com.vladsch.idea.multimarkdown.MultiMarkdownProjectComponent;
import com.vladsch.idea.multimarkdown.parser.MultiMarkdownLexParserManager;
import com.vladsch.idea.multimarkdown.settings.MultiMarkdownGlobalSettings;
import com.vladsch.idea.multimarkdown.settings.MultiMarkdownGlobalSettingsListener;
import com.vladsch.idea.multimarkdown.util.ReferenceChangeListener;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.pegdown.Extensions;
import org.pegdown.LinkRenderer;
import org.pegdown.ToHtmlSerializer;
import org.pegdown.ast.RootNode;

import javax.swing.*;
import javax.swing.text.DefaultCaret;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;
import java.awt.*;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.StringReader;
import java.util.Timer;
import java.util.TimerTask;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.vladsch.idea.multimarkdown.editor.MultiMarkdownPathResolver.isWikiDocument;
import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;

public class MultiMarkdownPreviewEditor extends UserDataHolderBase implements FileEditor {
    private static final Logger logger = Logger.getInstance(MultiMarkdownPreviewEditor.class);

    public static final String PREVIEW_EDITOR_NAME = MultiMarkdownBundle.message("multimarkdown.preview-tab-name");

    public static final String TEXT_EDITOR_NAME = MultiMarkdownBundle.message("multimarkdown.html-tab-name");

    protected static int instances = 0;

    /**
     * The {@link java.awt.Component} used to render the HTML preview.
     */
    protected final JEditorPane jEditorPane;

    /**
     * The {@link JBScrollPane} allowing to browse {@link #jEditorPane}.
     */
    protected final JBScrollPane scrollPane;

    /**
     * The {@link Document} previewed in this editor.
     */
    protected final Document document;
    protected final boolean isWikiDocument;

    //private final EditorTextField myTextViewer;
    private final EditorImpl myTextViewer;

    private boolean isReleased = false;

    protected MultiMarkdownGlobalSettingsListener globalSettingsListener;
    protected ReferenceChangeListener projectFileListener;

    private boolean isActive = false;

    private boolean isRawHtml = false;

    private boolean isEditorTabVisible = true;

    private Project project;

    private LinkRenderer linkRendererNormal;
    private LinkRenderer linkRendererModified;

    public static boolean isShowModified() {
        return MultiMarkdownGlobalSettings.getInstance().showHtmlTextAsModified.getValue();
    }

    public static int getParsingTimeout() {
        return MultiMarkdownGlobalSettings.getInstance().parsingTimeout.getValue();
    }

    public static int getUpdateDelay() {
        return MultiMarkdownGlobalSettings.getInstance().updateDelay.getValue();
    }

    public static boolean isTaskLists() {
        return MultiMarkdownGlobalSettings.getInstance().taskLists.getValue();
    }

    public static boolean isDarkTheme() {
        return MultiMarkdownGlobalSettings.getInstance().isDarkUITheme();
    }

    public static String getCustomCss() {
        return MultiMarkdownGlobalSettings.getInstance().customCss.getValue();
    }

    public static boolean isShowHtmlText() {
        return MultiMarkdownGlobalSettings.getInstance().showHtmlText.getValue();
    }

    /**
     * Indicates whether the HTML preview is obsolete and should regenerated from the Markdown {@link #document}.
     */
    protected boolean previewIsObsolete = true;

    protected Timer updateDelayTimer;
    protected final int instance = ++instances;

    protected void updateEditorTabIsVisible() {
        if (isRawHtml) {
            isEditorTabVisible = isShowHtmlText();
            getComponent().setVisible(isEditorTabVisible);
        } else {
            isEditorTabVisible = true;
        }
    }

    protected void checkNotifyUser() {
        //final Project project = this.project;
        //final MultiMarkdownGlobalSettings settings = MultiMarkdownGlobalSettings.getInstance();
        //
        //settings.startSuspendNotifications();
        //if (settings.isDarkUITheme() && (settings.iconBullets.getValue() || settings.iconTasks.getValue()) && true && !settings.wasShownDarkBug.getValue()) {
        //    // notify the user that the Icons for Tasks and Bullets will be turned off due to a rendering bug
        //    settings.wasShownDarkBug.setValue(true);
        //    NotificationGroup issueNotificationGroup = new NotificationGroup(MultiMarkdownGlobalSettings.NOTIFICATION_GROUP_ISSUES,
        //            NotificationDisplayType.BALLOON, true, null);
        //
        //    Notification notification = issueNotificationGroup.createNotification("<strong>MultiMarkdown</strong> Plugin Notification",
        //            "<p>An issue with rendering icons when the UI theme is <strong>Darcula</strong> prevents bullet "+
        //                    "and task list items from using these options. " +
        //                    "These settings will be ignored while <strong>Darcula</strong> "+
        //                    "theme is in effect and until the issue is fixed.</p>\n" +
        //                    "<p>&nbsp;</p>\n" +
        //                    "<p>Feel free leave the <em>Bullets with Icons</em> and <em>Tasks with Icons</em> options turned on. "+
        //                    "They will take effect when they no longer adversely affect the display.</p>\n" +
        //                    "",
        //            NotificationType.INFORMATION, null);
        //    notification.setImportant(true);
        //    Notifications.Bus.notify(notification, project);
        //}
        //settings.endSuspendNotifications();
    }

    protected void updateLinkRenderer() {
        int options = 0;
        if (MultiMarkdownGlobalSettings.getInstance().githubWikiLinks.getValue())
            options |= MultiMarkdownLinkRenderer.GITHUB_WIKI_LINK_FORMAT;
        linkRendererModified = new MultiMarkdownLinkRenderer(project, document, "absent", null,
                options | MultiMarkdownLinkRenderer.VALIDATE_LINKS);
        linkRendererNormal = new MultiMarkdownLinkRenderer(options);
    }

    /**
     * Build a new instance of {@link MultiMarkdownPreviewEditor}.
     *
     * @param project  the {@link Project} containing the document
     * @param document the {@link com.intellij.openapi.editor.Document} previewed in this editor.
     */
    public MultiMarkdownPreviewEditor(@NotNull final Project project, @NotNull Document document,
            boolean isRawHtml) {
        this.isRawHtml = isRawHtml;
        this.document = document;
        this.project = project;
        this.isWikiDocument = isWikiDocument(document);

        // Listen to the document modifications.
        this.document.addDocumentListener(new DocumentAdapter() {
            @Override
            public void documentChanged(DocumentEvent e) {
                delayedHtmlPreviewUpdate(false);
            }
        });

        // Listen to settings changes
        MultiMarkdownGlobalSettings.getInstance()
                .addListener(globalSettingsListener = new MultiMarkdownGlobalSettingsListener() {
                    public void handleSettingsChanged(@NotNull final MultiMarkdownGlobalSettings newSettings) {
                        if (project.isDisposed())
                            return;
                        updateEditorTabIsVisible();
                        updateLinkRenderer();
                        delayedHtmlPreviewUpdate(true);
                        checkNotifyUser();
                    }
                });

        MultiMarkdownProjectComponent projectComponent = MultiMarkdownPlugin.getProjectComponent(project);
        if (projectComponent != null) {
            projectComponent.addListener(projectFileListener = new ReferenceChangeListener() {
                @Override
                public void referenceChanged(@Nullable String name) {
                    if (project.isDisposed())
                        return;
                    delayedHtmlPreviewUpdate(false);
                }
            });
        }

        project.getMessageBus().connect(this).subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
            @Override
            public void enteredDumbMode() {
            }

            @Override
            public void exitDumbMode() {
                // need to re-evaluate class link accessibility
                if (project.isDisposed())
                    return;
                delayedHtmlPreviewUpdate(false);
            }
        });

        updateLinkRenderer();

        if (isRawHtml) {
            jEditorPane = null;
            scrollPane = null;
            Language language = Language.findLanguageByID("HTML");
            FileType fileType = language != null ? language.getAssociatedFileType() : null;
            //myTextViewer = new EditorTextField(EditorFactory.getInstance().createDocument(""), project, fileType, true, false);
            Document myDocument = EditorFactory.getInstance().createDocument("");
            myTextViewer = (EditorImpl) EditorFactory.getInstance().createViewer(myDocument, project);
            if (fileType != null)
                myTextViewer.setHighlighter(
                        EditorHighlighterFactory.getInstance().createEditorHighlighter(project, fileType));
        } else {
            // Setup the editor pane for rendering HTML.
            myTextViewer = null;
            jEditorPane = new JEditorPane();
            //jEditorPane = new BrowserPane();
            scrollPane = new JBScrollPane(jEditorPane);

            setStyleSheet();

            // Add a custom link listener which can resolve local link references.
            jEditorPane.addHyperlinkListener(new MultiMarkdownLinkListener(jEditorPane, project, document));
            jEditorPane.setEditable(false);

            // Set the editor pane caret position to top left, and do not let it reset it
            jEditorPane.getCaret().setMagicCaretPosition(new Point(0, 0));
            ((DefaultCaret) jEditorPane.getCaret()).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
        }

        checkNotifyUser();
    }

    protected String makeHtmlPage(String html) {
        VirtualFile file = FileDocumentManager.getInstance().getFile(document);
        // scan for <table>, </table>, <tr>, </tr> and other tags we modify, this could be done with a custom plugin to pegdown but
        // then it would be more trouble to get un-modified HTML.
        String regex = "(<table>|<thead>|<tbody>|<tr>|<hr/>|<del>|</del>|</p>|<kbd>|</kbd>|<var>|</var>";//|<code>|</code>";
        StringBuilder result = new StringBuilder(html.length() + (html.length() >> 2));

        String gitHubHref = MultiMarkdownPathResolver.getGitHubDocumentURL(project, document, !isWikiDocument);
        String gitHubClose = "";
        if (gitHubHref == null) {
            gitHubHref = "";
        } else {
            gitHubHref = "<a href=\"" + gitHubHref + "\" name=\"wikipage\" id=\"wikipage\">";
            gitHubClose = "</a>";
        }
        if (isWikiDocument) {
            result.append("<body class=\"multimarkdown-wiki-preview\">\n<div class=\"content\">\n");
            result.append("" + "<h1 class=\"first-child\">").append(gitHubHref)
                    .append(escapeHtml(file == null ? "" : file.getNameWithoutExtension().replace('-', ' ')))
                    .append(gitHubClose).append("</h1>\n").append("");
        } else {
            result.append("<body class=\"multimarkdown-preview\">\n<div class=\"content\">\n"
                    + "<div class=\"page-header\">").append(gitHubHref)
                    .append(escapeHtml(file == null ? "" : file.getName().replace('-', ' '))).append(gitHubClose)
                    .append("</div>\n").append("<div class=\"hr\"></div>\n").append("");
            // for now nothing
            regex += "|<h1>";
        }

        String regexTail = "|<li>\\n*\\s*<p>";
        boolean isDarkTheme = isDarkTheme();
        boolean taskLists = isTaskLists();

        if (taskLists) {
            regex += "|<li class=\"task-list-item\">\\n*\\s*<p>|<br\\s*/?>|<li class=\"task-list-item\">|<li>\\[(?:x|X)\\]\\s*|<li>\\[ \\]\\s*|<li>\\n*\\s*<p>\\[x\\]\\s*|<li>\\n*\\s*<p>\\[ \\]\\s*";
        }
        regex += regexTail;
        regex += ")";

        Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        Matcher m = p.matcher(html);
        int lastPos = 0;
        int rowCount = 0;
        boolean[] isOrderedList = new boolean[20];
        int listDepth = -1;
        boolean firstChildH1 = !isWikiDocument;

        while (m.find()) {
            String found = m.group();
            if (lastPos < m.start(0)) {
                result.append(html.substring(lastPos, m.start(0)));
            }

            if (found.equals("</p>")) {
                result.append(found);
            } else if (found.startsWith("<br")) {
                result.append("<br/>\n");
            } else if (found.equals("<table>")) {
                rowCount = 0;
                result.append(found);
            } else if (found.equals("<thead>")) {
                result.append(found);
            } else if (found.equals("<tbody>")) {
                result.append(found);
            } else if (found.equals("/>")) {
                result.append(">");
            } else if (found.equals("<tr>")) {
                rowCount++;
                result.append("<tr class=\"")
                        .append(rowCount == 1 ? "first-child" : (rowCount & 1) != 0 ? "odd-child" : "even-child")
                        .append("\">");
            } else if (found.equals("<hr/>")) {
                result.append("<div class=\"hr\">&nbsp;</div>");
            } else if (found.equals("<h1>")) {
                result.append(firstChildH1 ? "<h1 class=\"first-child\">" : "<h1>");
                firstChildH1 = false;
            } else if (found.equals("<del>")) {
                result.append("<span class=\"del\">");
            } else if (found.equals("</del>")) {
                result.append("</span>");
            } else if (found.equals("<kbd>")) {
                result.append("<span class=\"kbd\">");
            } else if (found.equals("</kbd>")) {
                result.append("</span>");
            } else if (found.equals("<code>")) {
                result.append("<span class=\"code\">");
            } else if (found.equals("</code>")) {
                result.append("</span>");
            } else if (found.equals("<var>")) {
                result.append("<span class=\"var\">");
            } else if (found.equals("</var>")) {
                result.append("</span>");
            } else {
                found = found.trim();
                if (taskLists && found.equals("<li>[x]")) {
                    result.append("<li class=\"dtask\">");
                } else if (taskLists && found.equals("<li>[X]")) {
                    result.append("<li class=\"dtask\">");
                } else if (taskLists && found.equals("<li>[ ]")) {
                    result.append("<li class=\"dtask\">");
                } else if (taskLists && found.equals("<li class=\"task-list-item\">")) {
                    result.append("<li class=\"taski\">");
                } else {
                    // here we have <li>\n*\s*<p>, need to strip out \n*\s* so we can match them easier
                    String foundWithP = found;
                    foundWithP = foundWithP.replaceAll("<li>\\n*\\s*<p>", "<li><p>");
                    found = foundWithP.replaceAll("<li class=\"task-list-item\">\\n*\\s*<p>",
                            "<li class=\"task\"><p>");
                    found = found.trim();
                    if (found.equals("<li><p>")) {
                        result.append("<li class=\"p\"><p class=\"p\">");
                    } else if (taskLists && found.equals("<li><p>[x]")) {
                        result.append("<li class=\"dtaskp\"><p class=\"p\">");
                    } else if (taskLists && found.equals("<li><p>[ ]")) {
                        result.append("<li class=\"dtaskp\"><p class=\"p\">");
                    } else if (taskLists && found.equals("<li class=\"task-list-item\"><p>")) {
                        result.append("<li class=\"taskp\"><p class=\"p\">");
                    } else {
                        result.append(found);
                    }
                }
            }

            lastPos = m.end(0);
        }

        if (lastPos < html.length()) {
            result.append(html.substring(lastPos));
        }

        result.append("\n</div>\n</body>\n");
        return result.toString();
    }

    protected void delayedHtmlPreviewUpdate(final boolean fullKit) {
        if (updateDelayTimer != null) {
            updateDelayTimer.cancel();
            updateDelayTimer = null;
        }

        if (project.isDisposed())
            return;

        if (!isEditorTabVisible)
            return;

        updateDelayTimer = new Timer();
        updateDelayTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (project.isDisposed())
                    return;

                ApplicationManager.getApplication().invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        if (project.isDisposed())
                            return;

                        previewIsObsolete = true;

                        if (fullKit) {
                            setStyleSheet();
                            //processor.remove();     // make it re-initialize when accessed
                        }

                        updateHtmlContent(isActive || isMyTabSelected());
                    }
                }, ModalityState.any());
            }
        }, getUpdateDelay());
    }

    protected boolean isMyTabSelected() {
        FileEditorManager manager = FileEditorManager.getInstance(project);
        FileEditor[] editors = manager.getSelectedEditors();
        for (FileEditor editor : editors) {
            if (editor == this)
                return true;
        }
        return false;
    }

    protected MultiMarkdownPreviewEditor findCounterpart() {
        // here we can find our HTML Text counterpart and update its HTML at the same time. but it is better to keep it separate for now
        VirtualFile file = FileDocumentManager.getInstance().getFile(document);
        if (file != null) {
            FileEditorManager manager = FileEditorManager.getInstance(project);
            FileEditor[] editors = manager.getEditors(file);
            for (FileEditor editor : editors) {
                if (editor != this && editor instanceof MultiMarkdownPreviewEditor) {
                    return (MultiMarkdownPreviewEditor) editor;
                }
            }
        }

        return null;
    }

    protected void setStyleSheet() {
        if (isRawHtml)
            return;

        MultiMarkdownEditorKit htmlKit = new MultiMarkdownEditorKit();

        final StyleSheet style = new MultiMarkdownStyleSheet();

        if (!MultiMarkdownGlobalSettings.getInstance().useCustomCss(false)) {
            style.importStyleSheet(MultiMarkdownGlobalSettings.getInstance().getCssFileURL(false));
        } else {
            try {
                style.loadRules(new StringReader(MultiMarkdownGlobalSettings.getInstance().getCssText(false)),
                        null);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        htmlKit.setStyleSheet(style);

        jEditorPane.setEditorKit(htmlKit);
    }

    public static void setStyleSheet(JEditorPane jEditorPane) {
        HTMLEditorKit htmlKit = new HTMLEditorKit();

        final StyleSheet style = new StyleSheet();

        if (!MultiMarkdownGlobalSettings.getInstance().useCustomCss(false)) {
            style.importStyleSheet(MultiMarkdownGlobalSettings.getInstance().getCssFileURL(false));
        } else {
            try {
                style.loadRules(new StringReader(MultiMarkdownGlobalSettings.getInstance().getCssText(false)),
                        null);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        htmlKit.setStyleSheet(style);
        jEditorPane.setEditorKit(htmlKit);
    }

    protected void updateRawHtmlText(final String htmlTxt) {
        final DocumentEx myDocument = myTextViewer.getDocument();

        if (project.isDisposed())
            return;

        ApplicationManager.getApplication().runWriteAction(new Runnable() {
            @Override
            public void run() {
                if (project.isDisposed())
                    return;

                CommandProcessor.getInstance().executeCommand(project, new Runnable() {
                    @Override
                    public void run() {
                        if (project.isDisposed())
                            return;

                        myDocument.replaceString(0, myDocument.getTextLength(), htmlTxt);
                        final CaretModel caretModel = myTextViewer.getCaretModel();
                        if (caretModel.getOffset() >= myDocument.getTextLength()) {
                            caretModel.moveToOffset(myDocument.getTextLength());
                        }
                    }
                }, null, null, UndoConfirmationPolicy.DEFAULT, myDocument);
            }
        });
    }

    protected String markdownToHtml(boolean modified, RootNode rootNode) {
        if (rootNode == null) {
            return "<strong>Parser timed out</strong>";
        } else {
            if (modified) {
                MultiMarkdownToHtmlSerializer htmlSerializer = new MultiMarkdownToHtmlSerializer(project, document,
                        linkRendererModified);

                if (!isWikiDocument) {
                    htmlSerializer.setFlag(MultiMarkdownToHtmlSerializer.NO_WIKI_LINKS);
                }

                return htmlSerializer.toHtml(rootNode);
            } else {
                return new ToHtmlSerializer(linkRendererNormal).toHtml(rootNode).replace("<br/>", "<br/>\n");
            }
        }
    }

    protected void updateHtmlContent(boolean force) {
        if (updateDelayTimer != null) {
            updateDelayTimer.cancel();
            updateDelayTimer = null;
        }

        if (previewIsObsolete && isEditorTabVisible && (isActive || force)) {
            try {
                int options = MultiMarkdownGlobalSettings.getInstance().getExtensionsValue();
                int pegdownExtensions = (options & ~Extensions.TASKLISTITEMS)
                        | ((options & Extensions.EXTANCHORLINKS) != 0 ? Extensions.EXTANCHORLINKS_WRAP : 0);
                RootNode rootNode = MultiMarkdownLexParserManager.parseMarkdownRoot(document.getCharsSequence(),
                        pegdownExtensions, getParsingTimeout());

                if (isRawHtml) {
                    updateRawHtmlText(isShowModified() ? makeHtmlPage(markdownToHtml(true, rootNode))
                            : markdownToHtml(false, rootNode));
                } else {
                    jEditorPane.setText(makeHtmlPage(markdownToHtml(true, rootNode)));
                }
                previewIsObsolete = false;

                // here we can find our HTML Text counterpart but it is better to keep it separate for now
                //VirtualFile file = FileDocumentManager.getInstance().getFile(document);
                //FileEditorManager manager = FileEditorManager.getInstance(project);
                //FileEditor[] editors = manager.getEditors(file);
                //for (int i = 0; i < editors.length; i++)
                //{
                //    if (editors[i] == this)
                //    {
                //        if (editors.length > i && editors[i+1] instanceof MultiMarkdownPreviewEditor) {
                //            // update its html too
                //            MultiMarkdownPreviewEditor htmlEditor = (MultiMarkdownPreviewEditor)editors[i+1];
                //            boolean showModified = MultiMarkdownGlobalSettings.getInstance().isShowHtmlTextAsModified();
                //            htmlEditor.setHtmlContent("<div id=\"multimarkdown-preview\">\n" + (showModified ? procHtml : html) + "\n</div>\n");
                //            break;
                //        }
                //    }
                //}
            } catch (Exception e) {
                logger.info("Failed processing Markdown document", e);
            }
        }
    }

    public void setHtmlContent(String html) {
        jEditorPane.setText(html);
    }

    /**
     * Get the {@link java.awt.Component} to display as this editor's UI.
     *
     * @return a scrollable {@link JEditorPane}.
     */
    @NotNull
    public JComponent getComponent() {
        return scrollPane != null ? scrollPane : myTextViewer.getComponent();
    }

    /**
     * Get the component to be focused when the editor is opened.
     *
     * @return {@link #scrollPane}
     */
    @Nullable
    public JComponent getPreferredFocusedComponent() {
        return scrollPane != null ? scrollPane : myTextViewer.getContentComponent();
    }

    /**
     * Get the editor displayable name.
     *
     * @return editor name
     */
    @NotNull
    @NonNls
    public String getName() {
        return isRawHtml ? TEXT_EDITOR_NAME : PREVIEW_EDITOR_NAME;
    }

    /**
     * Get the state of the editor.
     * <p/>
     * Just returns {@link FileEditorState#INSTANCE} as {@link MultiMarkdownPreviewEditor} is stateless.
     *
     * @param level the level.
     * @return {@link FileEditorState#INSTANCE}
     * @see #setState(com.intellij.openapi.fileEditor.FileEditorState)
     */
    @NotNull
    public FileEditorState getState(@NotNull FileEditorStateLevel level) {
        return FileEditorState.INSTANCE;
    }

    /**
     * Set the state of the editor.
     * <p/>
     * Does not do anything as {@link MultiMarkdownPreviewEditor} is stateless.
     *
     * @param state the new state.
     * @see #getState(com.intellij.openapi.fileEditor.FileEditorStateLevel)
     */
    public void setState(@NotNull FileEditorState state) {
    }

    /**
     * Indicates whether the document content is modified compared to its file.
     *
     * @return {@code false} as {@link MultiMarkdownPreviewEditor} is read-only.
     */
    public boolean isModified() {
        return false;
    }

    /**
     * Indicates whether the editor is valid.
     *
     * @return {@code true} if {@link #document} content is readable.
     */
    public boolean isValid() {
        return true;
    }

    /**
     * Invoked when the editor is selected.
     * <p/>
     * Update the HTML content if obsolete.
     */
    public void selectNotify() {
        isActive = true;
        if (previewIsObsolete) {
            updateHtmlContent(false);
        }
    }

    /**
     * Invoked when the editor is deselected.
     * <p/>
     * Does nothing.
     */
    public void deselectNotify() {
        isActive = false;
    }

    /**
     * Add specified listener.
     * <p/>
     * Does nothing.
     *
     * @param listener the listener.
     */
    public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
    }

    /**
     * Remove specified listener.
     * <p/>
     * Does nothing.
     *
     * @param listener the listener.
     */
    public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
    }

    /**
     * Get the background editor highlighter.
     *
     * @return {@code null} as {@link MultiMarkdownPreviewEditor} does not require highlighting.
     */
    @Nullable
    public BackgroundEditorHighlighter getBackgroundHighlighter() {
        return null;
    }

    /**
     * Get the current location.
     *
     * @return {@code null} as {@link MultiMarkdownPreviewEditor} is not navigable.
     */
    @Nullable
    public FileEditorLocation getCurrentLocation() {
        return null;
    }

    /**
     * Get the structure view builder.
     *
     * @return TODO {@code null} as parsing/PSI is not implemented.
     */
    @Nullable
    public StructureViewBuilder getStructureViewBuilder() {
        return null;
    }

    /**
     * Dispose the editor.
     */
    public void dispose() {
        if (!isReleased) {
            isReleased = true;

            if (updateDelayTimer != null) {
                updateDelayTimer.cancel();
                updateDelayTimer = null;
            }

            if (jEditorPane != null) {
                jEditorPane.removeAll();
            }

            if (globalSettingsListener != null) {
                MultiMarkdownGlobalSettings.getInstance().removeListener(globalSettingsListener);
                globalSettingsListener = null;
            }

            if (myTextViewer != null) {
                final Application application = ApplicationManager.getApplication();
                final Runnable runnable = new Runnable() {
                    @Override
                    public void run() {
                        if (!myTextViewer.isDisposed()) {
                            EditorFactory.getInstance().releaseEditor(myTextViewer);
                        }
                    }
                };

                if (application.isUnitTestMode() || application.isDispatchThread()) {
                    runnable.run();
                } else {
                    application.invokeLater(runnable);
                }
            }

            Disposer.dispose(this);
        }
    }
}