UNPKG

@phoenix-plugin-registry/brackets-markdown-preview

Version:

Markdown live preview incl. detached window, code syntax highlighting, output themes, adaptive preview width, graphical checkboxes, activation on start...

486 lines (419 loc) 22.6 kB
/** * @file Main file of the application implementing the extension "brackets-markdown-preview" * @author Loïs Bégué * @version 2.0.0 * @license MIT license (MIT) * @copyright Copyright (c) 2017 Loïs Bégué */ /*global define, brackets, $, window */ define(function (require, exports, module) { "use strict"; // Register Brackets modules var DocumentManager = brackets.getModule("document/DocumentManager"); var EditorManager = brackets.getModule("editor/EditorManager"); var ExtensionUtils = brackets.getModule("utils/ExtensionUtils"); var FileUtils = brackets.getModule("file/FileUtils"); var MainViewManager = brackets.getModule("view/MainViewManager"); var _ = brackets.getModule("thirdparty/lodash"); // Load templates as text var previewHTML = require("text!templates/preview.html"); // Register local 3rd party modules var marked = require("lib/marked"); var tinycolor = require("lib/tinycolor/tinycolor-min"); // Register local extension modules //var ExtUtils = require("scripts/ExtUtils"); var ExtPreferences = require("scripts/ExtPreferences"); var ExtPreviewPanel = require("scripts/ExtPreviewPanel"); var ExtCommander = require("scripts/ExtCommander"); var ExtHighlighting = require("scripts/ExtHighlighting"); // Register JQuery extension (once for the whole markdown preview extension) require("scripts/ExtJQuery"); // Define extension variables & constants var currrentMarkdownDocument; var currentMarkdownEditor; var previewIsActivated = false; // Define extension extended objects... var PreviewPreferences; var PreviewPanel; var ToolbarIcon; var PreviewMenuItem; /** * Enforce scrolling of the preview panel in sync with the editor, given the corresponding user setting is activated and the preview panel is visible. * @private */ function _markdownEditorScroll() { if (PreviewPreferences.syncScroll && PreviewPanel.isContentVisible()) { PreviewPanel.scroll(); } } /** * Highlight the code within an html document (fragment), given the corresponding user setting is activated. * @private * @param {string} source the html source code of the document to be highlighted * @returns {string} the highlighted html source code */ function _highlight(source) { let result = source; // Use highlighting on the document (if required), nevertheless if the document is only reloaded or not. // Highlighting is pre-computed/coded here - instead of attaching and running the highlight.js script within the (iFrame) client preview document. if (PreviewPreferences.highlightActive) { result = ExtHighlighting.highlightHTMLDocument(source); } return result; } /** * This procedure do generate a html preview of the input markdown document. * It first prepare the markdown source code and then transform it using the 'marked' library. * @private * @param {object} doc The source markdown document: a brackets object of type 'document' (see brackets api) * @param {boolean} isReload when this param is 'true', the function assumes that only the code of the source document has been modofied. * Hence it will the only generate (actualize) the body of the preview document. * When set to 'false' or ommited, this parameter enforce to recreate the complete preview document. */ function _generatePreview(doc, isReload) { if (doc) { //console.log("Generating preview of the markdown document [" + doc.file.name + "]"); //MainViewManager.getCurrentlyViewedPath()); var docText = doc.getText(), bodyText = "", yamlRegEx = /^-{3}([\w\W]+?)(-{3})/, yamlMatch = yamlRegEx.exec(docText); // If there's yaml front matter, remove it. if (yamlMatch) { docText = docText.substr(yamlMatch[0].length); } if (PreviewPreferences.markdownSanitizeAnchors) { // Sanitize Anchors in wrong format replacing "<a ... />" by "<a ... ></a>" (HTML5) docText = docText.replace(/(<a [^>]+)\/>/gmi, "$1></a>"); // Sanitize Anchors in wrong format, replacing "NAME" attribute by "ID" attribute (HTML5) docText = docText.replace(/(<(?:input|select|table|a)\b(?:\s+(?!name\b)[A-Za-z][\w\-:.]*(?:\s*=\s*(?:"[^"]*"| \'[^\']*\'| [\w\-:.]+))?)*)\s+name\s*=\s*("[^"]*"| \'[^\']*\'| [\w\-:.]+)/gmi, "$1 id=$2"); } // Parse markdown into HTML bodyText = marked(docText); // Show URL in link tooltip bodyText = bodyText.replace(/(href=\"([^\"]*)\")/g, "$1 title=\"$2\""); // Convert protocol-relative URLS bodyText = bodyText.replace(/src="\/\//g, "src=\"http://"); // Replace textual checkboxes by graphical ones within lists (task list items) when preference is set if (PreviewPreferences.graphicalCheckboxList) { bodyText = bodyText.replace(/<li>(<p>)?\[ ?\]/g, "<li>$1&#9744;"); bodyText = bodyText.replace(/<li>(<p>)?\[x\]/gi, "<li>$1&#9745;"); } if (isReload) { // highlight the body text bodyText = _highlight(bodyText); // only the body content must be actualized. the html framework should not need to be reloaded as long as the settings weren't modified PreviewPanel.setHTMLContent(bodyText); } else { // Calculate the <base> URL for relative URIs var baseUrl = window.location.protocol + "//" + FileUtils.getDirectoryPath(doc.file.fullPath); // Save the name of the edited document var documentName = doc.file.name; // Calculate the URL of the CSS theme file name var themeUrl = window.location.protocol + "//" + require.toUrl("./themes/" + PreviewPreferences.theme + ".css"); // Calculate the URL of the preview CSS file name (containing Graphical Checkbox code ...) var previewStyleUrl = window.location.protocol + "//" + require.toUrl("./styles/preview.css"); // Calculate the style of the body element to reflect the state of the adaptiveWidth flag // Note: width and max-width have both to be set (due to some css style themes, that could override those) ! var adaptiveWidthStyle = PreviewPreferences.adaptiveWidth ? "adaptive" : ""; // Calculate the URL of the highlight CSS theme file name var highlightThemeUrl = window.location.protocol + "//" + require.toUrl("./lib/highlight.js/styles/" + PreviewPreferences.highlightTheme + ".css"); // Calculate the background color of the preview, trying to override the theme specific color value // Note: an empty value will remove the style setting var backgroundColor = ""; var backgroundColorIsValid = PreviewPreferences.backgroundColor ? tinycolor(PreviewPreferences.backgroundColor).isValid() : false; if (backgroundColorIsValid) { backgroundColor = "background-color: " + tinycolor(PreviewPreferences.backgroundColor).toHexString(); } // output character encoding var outputCharacterEncoding = PreviewPreferences.outputCharacterEncoding ? "<meta charset=" + PreviewPreferences.outputCharacterEncoding + ">" : ""; // Assemble the HTML source let htmlSource = _.template(previewHTML)({ baseUrl : baseUrl, themeUrl : themeUrl, bodyText : bodyText, adaptiveWidthStyle : adaptiveWidthStyle, previewStyleUrl : previewStyleUrl, documentName : documentName, highlightThemeUrl : highlightThemeUrl, backgroundColor : backgroundColor, outputCharacterEncoding : outputCharacterEncoding }); // highlight the html source code htmlSource = _highlight(htmlSource); // load the html souce code into the preview panel PreviewPanel.setHTMLSource(htmlSource, () => { _markdownEditorScroll(); PreviewPanel.showContent(); }); } } } /** * This is the callback attached to the editor displaying the source markdown document. * It is triggered when the source is changed/edited. Hence the function re-generates the preview. * @private * @param {object} e the event fired by the brackets 'document' object containing the instance of the 'document' as event 'target'. */ function _markdownDocumentChange(e) { _generatePreview(e.target, true); } /** * This is the callback attached to the 'change' event of the settings elements within the settings dialog. * When a setting is modified, the preview is actualized accordingly. * @private */ function _updateSettings() { // if using GFM while the adaptive width mode is active, then disable the "break" option. let useGFM = PreviewPreferences.useGFM; let useAdaptiveWidth = PreviewPreferences.adaptiveWidth; let useBreaks = useGFM && !useAdaptiveWidth; marked.setOptions({ breaks: useBreaks, gfm: useGFM }); // Re-render _generatePreview(currrentMarkdownDocument); } /** * Callback that is fired to initialize the settings dialog = set the "change" event handlers. * This function will be called only once at the time the dialog is beeing initialized. * It binds the preferences with the corresponding DOM elements = defines the change events of those elements. * @private * @param {object} dialog the JQuery element representing the DOM part of the setting dialog. */ function _doInitializeSettingsWithPreferences(dialog){ // Preview engine format (marked options) setting dialog.find("#markdown-preview-format") .change(function (e) { PreviewPreferences.useGFM = (e.target.selectedIndex === 1); _updateSettings(); }); // Preview CSS theme setting dialog.find("#markdown-preview-theme") .change(function (e) { PreviewPreferences.theme = e.target.value; _updateSettings(); }); // Preview scrolling synchronization setting dialog.find("#markdown-preview-sync-scroll") .change(function (e) { PreviewPreferences.syncScroll = e.target.checked; _markdownEditorScroll(); }); // Adaptive Preview Width setting dialog.find("#markdown-preview-adaptive-width") .change(function (e) { PreviewPreferences.adaptiveWidth = e.target.checked; _updateSettings(); }); // Graphical checkbox list setting dialog.find("#markdown-graphical-checkbox-list") .change(function (e) { PreviewPreferences.graphicalCheckboxList = e.target.checked; _updateSettings(); }); // Graphical checkbox list setting dialog.find("#markdown-sanitize-anchors") .change(function (e) { PreviewPreferences.markdownSanitizeAnchors = e.target.checked; _updateSettings(); }); // Highlight CSS theme setting dialog.find("#markdown-highlight-theme") .change(function (e) { PreviewPreferences.highlightTheme = e.target.value; _updateSettings(); }); // Activation of code block highlighting setting dialog.find("#markdown-highlight-active") .change(function (e) { PreviewPreferences.highlightActive = e.target.checked; _updateSettings(); }); // Preview background color setting dialog.find("#markdown-background-color") .change(function (e) { // Any valid values will set the color. If the value is missing (empty string), then the setting is reseted. // Note: tinycolor("") => #000000 which is not suitable... hence the test for an empty string. var colorValue = e.target.value.toString(); if (tinycolor(colorValue).isValid()) { PreviewPreferences.backgroundColor = tinycolor(colorValue).toHexString(); _updateSettings(); } else if (colorValue === "") { PreviewPreferences.backgroundColor = ""; _updateSettings(); } }); // Activate preview on start setting dialog.find("#markdown-activate-preview-on-start") .change(function (e) { PreviewPreferences.activatePreviewOnStart = e.target.checked; }); // Output character encoding setting dialog.find("#markdown-output-character-encoding") .change(function (e) { PreviewPreferences.outputCharacterEncoding = e.target.value; _updateSettings(); }); } /** * This function is aclled everytime the settings dialog is about to be displayed: * It initilize the DOM elements according to the corresponding preferences. * @private * @param {object} dialog the JQuery element representing the DOM part of the setting dialog. */ function _doLoadPreferencesValues(dialog) { // Preview engine format (marked options) setting dialog.find("#markdown-preview-format").prop("selectedIndex", PreviewPreferences.useGFM ? 1 : 0); // Preview CSS theme setting dialog.find("#markdown-preview-theme").val(PreviewPreferences.theme); // Preview scrolling synchronization setting dialog.find("#markdown-preview-sync-scroll").attr("checked", PreviewPreferences.syncScroll); // Adaptive Preview Width setting dialog.find("#markdown-preview-adaptive-width").attr("checked", PreviewPreferences.adaptiveWidth); // Graphical checkbox list setting dialog.find("#markdown-graphical-checkbox-list").attr("checked", PreviewPreferences.graphicalCheckboxList); // Graphical checkbox list setting dialog.find("#markdown-sanitize-anchors").attr("checked", PreviewPreferences.markdownSanitizeAnchors); // Highlight CSS theme setting dialog.find("#markdown-highlight-theme").val(PreviewPreferences.highlightTheme); // Activation of code block highlighting setting dialog.find("#markdown-highlight-active").attr("checked", PreviewPreferences.highlightActive); // Preview background color setting dialog.find("#markdown-background-color").val(PreviewPreferences.backgroundColor); // Activate preview on start setting dialog.find("#markdown-activate-preview-on-start").attr("checked", PreviewPreferences.activatePreviewOnStart); // Preview output character encoding dialog.find("#markdown-output-character-encoding").val(PreviewPreferences.outputCharacterEncoding); // Bind the "detached preview" button to the corresponding event dialog.find("#detached-preview-button").click(function(){ PreviewPanel.showDetachedPreview(); }); } /** * This function ensure that the preview panel is displayed or hidden according to the input parameter. * If the preview should be visible, then the function actualize the preview content. * @private * @param {boolean} makeVisible 'true' if the preview should be visible. 'false' otherwise. */ function _setPreviewPanelVisibility(makeVisible) { if (makeVisible) { ToolbarIcon.activate(); PreviewPanel.showPanel(); _generatePreview(DocumentManager.getCurrentDocument()); } else { ToolbarIcon.deactivate(); PreviewPanel.hidePanel(); PreviewPanel.hideContent(); } } /** * This function ensure the availability of the preview panel according to the file extension of the currently edited document. * Callback to the 'currentFileChange' event triggered by the Brackets MainViewManager everytime the file currently displayed in the editor is changed. * Notes: The preview panel may not be visible (depending on the current preview activation) but the preview is 'available'. * If the file is a markdown document, then the toolbar icon is made visible and the menu item is activated. * In other case, they are hidden/deactivated. * @private */ function _currentFileChangeHandler() { let doc = DocumentManager.getCurrentDocument(); let ext = doc ? FileUtils.getFileExtension(doc.file.fullPath).toLowerCase() : ""; if (currrentMarkdownDocument) { currrentMarkdownDocument.off("change", _markdownDocumentChange); currrentMarkdownDocument = null; } if (currentMarkdownEditor) { currentMarkdownEditor.off("scroll", _markdownEditorScroll); currentMarkdownEditor = null; } // test the extension of the displayed document and enable or disable the preview accordingly. // the following RegEx should at least check for all the following extensions: // ".markdown", ".mdown", ".mkdn", ".md", ".mkd", ".mdwn", ".mdtxt", ".mdtext", ".text", ".Rmd" // See ref. https://superuser.com/questions/249436/file-extension-for-markdown-files/285878#285878 if (doc && /litcoffee|markdown|md|mkd|text|txt/.test(ext)) { ToolbarIcon.showIcon(); PreviewMenuItem.setEnabled(true); currrentMarkdownDocument = doc; currrentMarkdownDocument.on("change", _markdownDocumentChange); currentMarkdownEditor = EditorManager.getCurrentFullEditor(); currentMarkdownEditor.on("scroll", _markdownEditorScroll); _setPreviewPanelVisibility(previewIsActivated); } else { ToolbarIcon.hideIcon(); PreviewMenuItem.setEnabled(false); _setPreviewPanelVisibility(false); } } /** * This function activate/deactivate the preview panel by setting the corresponding global "previewIsActivated" state variable. * This variable is used by other functions to act accordingly. * @private * @param {boolean} activate 'true' if the preview should be activated. It doesn't mean, that the panel is visible, though. * 'false' if the preview should not be active, hence the menu item should be unchecked. */ function _activatePreview(activate){ previewIsActivated = activate; PreviewMenuItem.setChecked(activate); } /** * This callback toggles the visibility of the preview panel and the state of both the menu item and the toolbar icon. * @private */ function _toggleMarkdownPreview() { _activatePreview(!previewIsActivated); _setPreviewPanelVisibility(previewIsActivated); } /** * This function instanciate the extension specific preferences. * The preferences are made available as properties of the PreviewPreferences object. */ function createPreferences(){ PreviewPreferences = ExtPreferences.Preferences; } /** * This function instanciate the preview panel and the settings dialog. */ function createPreviewPanel(){ PreviewPanel = ExtPreviewPanel.createPreviewPanel("brackets-markdown-preview", _doInitializeSettingsWithPreferences, _doLoadPreferencesValues); } /** * This function instanciate the ToolbarIcon that allows to toggle the preview panel. */ function createToolbarIcon(){ ToolbarIcon = ExtCommander.createToolbarIcon("brackets-markdown-preview-icon", _toggleMarkdownPreview); } /** * This function instanciate the Brackets MenuItem that allows to toggle the preview panel. */ function createMenuItem(){ PreviewMenuItem = ExtCommander.createMenuItem("Markdown preview", "brackets-markdown-preview.toogle-markdown-preview", _toggleMarkdownPreview); } /** * This function runs some 'self' tests provided by some extension modules. * The 'Self' tests mainly consist in some outputs to the console according to the respective module specific function calls. */ function runSelfTests(){ // TODO: create a user preference setting to enable/disable (reset to run only once) running the self tests... // execute some tests... //var ExtLoggingUtils = require("scripts/ExtLoggingUtils"); //ExtLoggingUtils.selfTest(); //ExtHighlighting.selfTest(); //ExtPreferences.selfTest(); } /*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ *START LOADING AND CONFIGURING EXTENSION ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ // Debounce event callback to avoid excess overhead // Update preview 300 ms after document change // Sync scroll 1 ms after document scroll (just enough to ensure // the document scroll isn't blocked). _markdownDocumentChange = _.debounce(_markdownDocumentChange, 300); _markdownEditorScroll = _.debounce(_markdownEditorScroll, 1); // Insert CSS for this extension ExtensionUtils.loadStyleSheet(module, "styles/extension.css"); // Initialize... createPreferences(); createToolbarIcon(); createMenuItem(); createPreviewPanel(); runSelfTests(); // Should the preview be activated on brackets start/reload? _activatePreview(PreviewPreferences.activatePreviewOnStart); // Add a document change handler MainViewManager.on("currentFileChange", _currentFileChangeHandler); });