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

Java tutorial

Introduction

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

Source

/*
 * 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.
 *
 * This file is based on the IntelliJ SimplePlugin tutorial
 *
 */
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.Disposable;
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.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.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.GitHubLinkResolver;
import com.vladsch.idea.multimarkdown.util.ReferenceChangeListener;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.web.PopupFeatures;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.util.Callback;
import netscape.javascript.JSException;
import netscape.javascript.JSObject;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.pegdown.LinkRenderer;
import org.pegdown.PegDownProcessor;
import org.pegdown.ToHtmlSerializer;
import org.pegdown.ast.RootNode;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;

import javax.swing.*;
import java.awt.*;
import java.beans.PropertyChangeListener;
import java.util.Timer;
import java.util.TimerTask;

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

//import com.sun.javafx.scene.layout.region.CornerRadiiConverter;

public class MultiMarkdownFxPreviewEditor extends UserDataHolderBase implements FileEditor, Disposable {
    protected static final org.apache.log4j.Logger logger = org.apache.log4j.Logger
            .getLogger(MultiMarkdownFxPreviewEditor.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 Component} used to render the HTML preview.
     */
    protected final JPanel jEditorPane;
    protected WebView webView;
    protected WebEngine webEngine;
    protected JFXPanel jfxPanel;
    protected String scrollOffset = null;
    protected AnchorPane anchorPane;

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

    //protected final EditorTextField myTextViewer;
    protected final EditorImpl myTextViewer;

    protected boolean isReleased = false;

    protected MultiMarkdownGlobalSettingsListener globalSettingsListener;
    protected ReferenceChangeListener projectFileListener;

    /**
     * The {@link PegDownProcessor} used for building the document AST.
     */
    //private ThreadLocal<PegDownProcessor> processor = initProcessor();
    private PegDownProcessor processor = null;

    protected boolean isActive = false;

    protected boolean isRawHtml = false;

    protected boolean isEditorTabVisible = true;

    protected Project project;

    protected LinkRenderer linkRendererNormal;
    protected LinkRenderer linkRendererModified;
    protected String pageScript = null;
    protected boolean needStyleSheetUpdate;
    protected boolean htmlWorkerRunning;

    protected String fireBugJS;
    private final VirtualFile containingFile;
    private GitHubLinkResolver resolver;

    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().customFxCss.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 MultiMarkdownFxPreviewEditor}.
     *
     * @param project the {@link Project} containing the document
     * @param doc     the {@link Document} previewed in this editor.
     */
    public MultiMarkdownFxPreviewEditor(@NotNull final Project project, @NotNull Document doc, boolean isRawHtml) {
        this.isRawHtml = isRawHtml;
        this.document = doc;
        this.project = project;
        this.isWikiDocument = isWikiDocument(document);
        containingFile = FileDocumentManager.getInstance().getFile(document);
        resolver = containingFile == null ? null : new GitHubLinkResolver(containingFile, project);

        // 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;
                        processor = null;
                        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;
            jfxPanel = null;
            webView = null;
            webEngine = null;
            Language language = Language.findLanguageByID("HTML");
            FileType fileType = language != null ? language.getAssociatedFileType() : null;
            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 JPanel(new BorderLayout(), false);
            jfxPanel = new JFXPanel(); // initializing javafx
            jEditorPane.add(jfxPanel, BorderLayout.CENTER);
            Platform.setImplicitExit(false);

            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    if (project.isDisposed())
                        return;

                    webView = new WebView();
                    webEngine = webView.getEngine();

                    anchorPane = new AnchorPane();
                    AnchorPane.setTopAnchor(webView, 0.0);
                    AnchorPane.setLeftAnchor(webView, 0.0);
                    AnchorPane.setBottomAnchor(webView, 0.0);
                    AnchorPane.setRightAnchor(webView, 0.0);
                    anchorPane.getChildren().add(webView);
                    jfxPanel.setScene(new Scene(anchorPane));

                    webEngine.setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() {
                        @Override
                        public WebEngine call(PopupFeatures config) {
                            // return a web engine for the new browser window or null to block popups
                            return null;
                        }
                    });

                    addStateChangeListener();
                }
            });
        }

        checkNotifyUser();
    }

    protected void addStateChangeListener() {
        webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> observable, Worker.State oldState,
                    Worker.State newState) {
                if (project.isDisposed())
                    return;
                workerStateChanged(observable, oldState, newState);
            }
        });
    }

    protected void workerStateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState,
            Worker.State newState) {
        //logger.info("[" + instance + "] " + "newState: " + newState + ", oldState: " + oldState);
        if (newState == Worker.State.SUCCEEDED) {
            // restore scroll if we had it
            JSObject jsobj = (JSObject) webEngine.executeScript("window");
            jsobj.setMember("java", new JSBridge(this));

            EventListener listener = new EventListener() {
                @Override
                public void handleEvent(org.w3c.dom.events.Event evt) {
                    evt.stopPropagation();
                    evt.preventDefault();

                    if (project.isDisposed())
                        return;

                    Element link = (Element) evt.getCurrentTarget();
                    org.w3c.dom.Document doc = webEngine.getDocument();
                    final String href = link.getAttribute("href");
                    if (href.charAt(0) == '#') {
                        if (href.length() != 1) {
                            // tries to go to an anchor
                            String hrefName = href.substring(1);
                            // scroll it into view
                            try {
                                JSObject result = (JSObject) webEngine.executeScript("(function () {\n"
                                        + "    var elemTop = 0;\n" + "    var elems = '';\n"
                                        + "    var elem = window.document.getElementById('" + hrefName + "');\n"
                                        + "    if (!elem) {\n"
                                        + "        var elemList = window.document.getElementsByTagName('a');\n"
                                        + "        for (a in elemList) {\n"
                                        + "            var aElem = elemList[a]\n"
                                        + "            if (aElem.hasOwnProperty('name') && aElem.name == '"
                                        + hrefName + "') {\n" + "                elem = aElem;\n"
                                        + "                break;\n" + "            }\n" + "        }\n" + "    }\n"
                                        + "    if (elem) {\n"
                                        + "        while (elem && elem.tagName !== 'HTML') {\n"
                                        + "            elems += ',' + elem.tagName + ':' + elem.offsetTop\n"
                                        + "            if (elem.offsetTop) {\n"
                                        + "                elemTop += elem.offsetTop;\n"
                                        + "                break;\n" + "            }\n"
                                        + "            elem = elem.parentNode\n" + "        }\n" + "    }\n"
                                        + "    return { elemTop: elemTop, elems: elems, found: !!elem };\n" + "})()"
                                        + "");
                                int elemTop = (Integer) result.getMember("elemTop");
                                boolean elemFound = (Boolean) result.getMember("found");
                                String parentList = (String) result.getMember("elems");
                                //logger.trace(parentList);
                                if (elemFound)
                                    webEngine.executeScript("window.scroll(0, " + elemTop + ")");
                            } catch (JSException ex) {
                                String error = ex.toString();
                                logger.info("[" + instance + "] " + "JSException on script", ex);
                            }
                        }
                    } else {
                        MultiMarkdownPathResolver.launchExternalLink(project, href);
                    }
                }
            };

            NodeList nodeList;
            org.w3c.dom.Document doc = webEngine.getDocument();
            if (doc != null) {
                ((EventTarget) doc.getDocumentElement()).addEventListener("contextmenu", new EventListener() {
                    @Override
                    public void handleEvent(Event evt) {
                        evt.preventDefault();
                    }
                }, false);

                Element el = doc.getElementById("a");
                nodeList = doc.getElementsByTagName("a");
                for (int i = 0; i < nodeList.getLength(); i++) {
                    ((EventTarget) nodeList.item(i)).addEventListener("click", listener, false);
                }

                // all images are mapped during conversion. Any relative ones are not resolved.
                //nodeList = doc.getElementsByTagName("img");
                //for (int i = 0; i < nodeList.getLength(); i++) {
                //    HTMLImageElementImpl imgNode = (HTMLImageElementImpl) nodeList.item(i);
                //    String src = imgNode.getSrc();
                //    if (!src.startsWith("http://") && !src.startsWith("https://") && !src.startsWith("ftp://") && !src.startsWith("file://")) {
                //        // relative to document, change it to absolute file://
                //        // this means it does not resolve, leave it
                //        if (!project.isDisposed() && containingFile != null && resolver != null) {
                //            ImageLinkRef linkRef = new ImageLinkRef(new FileRef(containingFile), src, null, null);
                //            PathInfo resolvedTarget = resolver.resolve(linkRef, LinkResolver.ONLY_URI, null);
                //
                //            assert resolvedTarget == null || resolvedTarget instanceof LinkRef && linkRef.isURI() : "Expected URI LinkRef, got " + linkRef;
                //            if (resolvedTarget != null) {
                //                imgNode.setSrc(resolvedTarget.getFilePath());
                //            }
                //        }
                //    }
                //}
            }

            if (pageScript != null && pageScript.length() > 0) {
                webEngine.executeScript(pageScript);
            }

            // enable debug if it is enabled in settings
            if (MultiMarkdownGlobalSettings.getInstance().enableFirebug.getValue()) {
                enableDebug();
            }

            String scroll = scrollOffset;
            if (scroll != null) {
                try {
                    //webEngine.executeScript("window.java.log('test info')");
                    webEngine.executeScript(
                            "" + "window.setTimeout(function () { " + "    window.java.log('before scroll');"
                                    + "    " + scroll + ";\n" + "    window.java.log('after scroll');" + "}, 50);");
                } catch (Exception e) {
                    logger.info("[" + instance + "] " + "JSException on script", e);
                }
            }

            try {
                webEngine.executeScript("window.addEventListener('scroll', function() { "
                        + "    window.java.onScroll();" + "    window.setTimeout(function () { "
                        + "        window.java.repaint()" + "    }, 100);" + "})");
            } catch (Exception e) {
                logger.info("[" + instance + "] " + "", e);
            }

            //if (needStyleSheetUpdate) {
            //    setStyleSheet();
            //}
        }

        if (newState == Worker.State.READY || newState == Worker.State.FAILED
                || newState == Worker.State.SUCCEEDED) {
            htmlWorkerRunning = false;
        }
    }

    // call backs from JavaScript will be handled by the bridge
    public static class JSBridge {
        final MultiMarkdownFxPreviewEditor editor;

        public JSBridge(MultiMarkdownFxPreviewEditor editor) {
            this.editor = editor;
        }

        public void log(String msg) {
            //logger.info("[" + editor.instance + "] " + msg);
        }

        public void repaint() {
            //logger.info("[" + editor.instance + "] " + "before repaint");
            //jEditorPane.invalidate();
            if (editor.project.isDisposed())
                return;

            editor.jfxPanel.repaint();
            //logger.info("[" + editor.instance + "] " + "after repaint");
        }

        public void onScroll() {
            if (editor.project.isDisposed())
                return;

            JSObject scrollPos = (JSObject) editor.webEngine
                    .executeScript("({ x: window.pageXOffset, y: window.pageYOffset })");
            //logger.info("[" + editor.instance + "] " + "window scrolled to " + scrollPos.getMember("x") + ", " + scrollPos.getMember("y"));
            editor.scrollOffset = "window.scroll(" + scrollPos.getMember("x") + ", " + scrollPos.getMember("y")
                    + ")";
        }
    }

    protected String makeHtmlPage(String html) {
        VirtualFile file = FileDocumentManager.getInstance().getFile(document);
        String result = "<head>\n" + "";

        final MultiMarkdownGlobalSettings globalSettings = MultiMarkdownGlobalSettings.getInstance();

        // load colors css
        if (!(globalSettings.useCustomCss(true) && globalSettings.includesColorsCss.getValue())) {
            result += "" + "<link rel=\"stylesheet\" href=\"" + globalSettings.getColorsCssExternalForm(true)
                    + "\">\n" + "";
        }

        // load layout css
        if (!(globalSettings.useCustomCss(true) && globalSettings.includesLayoutCss.getValue())) {
            result += "" + "<link rel=\"stylesheet\" href=\"" + globalSettings.getLayoutCssExternalForm(true)
                    + "\">\n" + "";
        }

        // load highlight & css
        if (globalSettings.useHighlightJs.getValue()) {
            if (!(globalSettings.useCustomCss(true) && globalSettings.includesHljsCss.getValue())) {
                result += "" + "<link rel=\"stylesheet\" href=\"" + globalSettings.getHljsCssExternalForm(true)
                        + "\">\n" + "";
            }
        }

        // load custom css
        if (globalSettings.useCustomCss(true)) {
            result += "" + "<link rel=\"stylesheet\" href=\"" + globalSettings.getCustomCssExternalForm(true)
                    + "\">\n" + "";
        }

        // load highlight js script
        if (globalSettings.useHighlightJs.getValue()) {
            result += "" + "<script src=\"" + globalSettings.getHighlighJsExternalForm(true) + "\"></script>\n"
                    + "";
        }

        result += "" + "<title>" + escapeHtml(file != null ? file.getName() : "<null>") + "</title>\n" + "</head>\n"
                + "<body>\n" + "";

        String gitHubHref = MultiMarkdownPathResolver.getGitHubDocumentURL(project, document, !isWikiDocument);
        String gitHubClose = "";
        if (isWikiDocument) {
            if (gitHubHref == null) {
                gitHubHref = "";
            } else {
                gitHubHref = "<a href=\"" + gitHubHref
                        + "\" name=\"wikipage\" id=\"wikipage\" class=\"anchor\"><span class=\"octicon octicon-link\"></span>";
                gitHubClose = "</a>";
            }

            result += "" + "<div class=\"wiki-container\">\n" + "<h1>" + gitHubHref + gitHubClose
                    + escapeHtml(file != null ? file.getNameWithoutExtension().replace('-', ' ') : "<null>")
                    + "</h1>\n" + "<article class=\"wiki-body\">\n" + "";
        } else {
            if (gitHubHref == null) {
                gitHubHref = "";
            } else {
                gitHubHref = "<a href=\"" + gitHubHref
                        + "\" name=\"markdown-page\" id=\"markdown-page\" class=\"page-anchor\">";
                gitHubClose = "</a>";
            }
            result += "" + "<div class=\"container\">\n" + "<div id=\"readme\" class=\"boxed-group\">\n" + "<h3>\n"
                    + "   " + gitHubHref + "<span class=\"bookicon octicon-book\"></span>\n" + gitHubClose + "  "
                    + (file != null ? file.getName() : "<null>") + "\n" + "</h3>\n"
                    + "<article class=\"markdown-body\">\n" + "";
        }

        result += html;
        result += "\n</article>\n";
        result += "</div>\n";
        result += "</div>\n";
        result += "" + "<script>hljs.initHighlightingOnLoad();</script>\n" + "</body>\n";
        return result;
    }

    public void enableDebug() {
        try {
            webEngine.executeScript("if (!document.getElementById('FirebugLite')) {\n"
                    + "    E = document['createElement' + 'NS'] && document.documentElement.namespaceURI;\n"
                    + "    E = E ? document['createElement' + 'NS'](E, 'script') : document['createElement']('script');\n"
                    + "    E['setAttribute']('id', 'FirebugLite');\n"
                    + "    E['setAttribute']('src', 'https://getfirebug.com/' + 'firebug-lite.js' + '#startOpened');\n"
                    +
                    //"    E['setAttribute']('src', '" + fireBugJS + "');\n" +
                    "    E['setAttribute']('FirebugLite', '4');\n"
                    + "    (document['getElementsByTagName']('head')[0] || document['getElementsByTagName']('body')[0]).appendChild(E);\n"
                    + "    E = new Image;\n"
                    + "    E['setAttribute']('src', 'https://getfirebug.com/' + '#startOpened');\n" + "}\n");
        } catch (JSException ex) {
            String error = ex.toString();
            logger.info("[" + instance + "] " + "JSException on script", ex);
        }
    }

    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) {
                            needStyleSheetUpdate = true;
                            //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 MultiMarkdownFxPreviewEditor 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 MultiMarkdownFxPreviewEditor) {
                    return (MultiMarkdownFxPreviewEditor) editor;
                }
            }
        }

        return null;
    }

    //protected void setStyleSheet() {
    //    if (isRawHtml) return;
    //
    //    needStyleSheetUpdate = false;
    //    webEngine.setUserStyleSheetLocation(MultiMarkdownGlobalSettings.getInstance().getCssExternalForm());
    //}

    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).replace("<br/>", "<br/>\n");
            } 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 {
                final RootNode rootNode = MultiMarkdownLexParserManager.parseMarkdownRoot(
                        document.getCharsSequence(), MultiMarkdownGlobalSettings.getInstance().getExtensionsValue(),
                        getParsingTimeout());
                if (isRawHtml) {
                    final String htmlTxt = isShowModified() ? makeHtmlPage(markdownToHtml(true, rootNode))
                            : markdownToHtml(false, rootNode);
                    updateRawHtmlText(htmlTxt);
                } else {
                    if (!htmlWorkerRunning) {
                        htmlWorkerRunning = true;
                        previewIsObsolete = false;

                        final String html = makeHtmlPage(markdownToHtml(true, rootNode));
                        Platform.runLater(new Runnable() {
                            @Override
                            public void run() {
                                if (project.isDisposed())
                                    return;

                                // TODO: add option to enable/disable keeping scroll position on update
                                Worker.State state = webEngine.getLoadWorker().getState();
                                //logger.info("[" + instance + "] " + "State on update " + state);
                                Double pageZoom = MultiMarkdownGlobalSettings.getInstance().pageZoom.getValue();
                                if (webView.getZoom() != pageZoom) {
                                    //logger.info("[" + instance + "] " + "setZoom(" + pageZoom + ")");
                                    webView.setZoom(pageZoom);
                                }

                                //logger.info("[" + instance + "] " + "loadContent");
                                webEngine.loadContent(html);
                            }
                        });
                    } else {
                        // reschedule the update for later
                        delayedHtmlPreviewUpdate(false);
                    }
                }
            } catch (Exception e) {
                logger.info("[" + instance + "] " + "Failed processing Markdown document", e);
            }
        }
    }

    @NotNull
    public JComponent getComponent() {
        //return scrollPane != null ? scrollPane : myTextViewer.getComponent();
        return jEditorPane != null ? jEditorPane : myTextViewer.getComponent();
    }

    @Nullable
    public JComponent getPreferredFocusedComponent() {
        //return scrollPane != null ? scrollPane : myTextViewer.getComponent();
        return jEditorPane != null ? jEditorPane : myTextViewer.getContentComponent();
    }

    @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 MultiMarkdownFxPreviewEditor} is stateless.
     *
     * @param level the level.
     * @return {@link FileEditorState#INSTANCE}
     * @see #setState(FileEditorState)
     */
    @NotNull
    public FileEditorState getState(@NotNull FileEditorStateLevel level) {
        return FileEditorState.INSTANCE;
    }

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

    /**
     * Indicates whether the document content is modified compared to its file.
     *
     * @return {@code false} as {@link MultiMarkdownFxPreviewEditor} 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 MultiMarkdownFxPreviewEditor} does not require highlighting.
     */
    @Nullable
    public BackgroundEditorHighlighter getBackgroundHighlighter() {
        return null;
    }

    /**
     * Get the current location.
     *
     * @return {@code null} as {@link MultiMarkdownFxPreviewEditor} 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);
        }
    }
}