@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
JavaScript
/**
* @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☐");
bodyText = bodyText.replace(/<li>(<p>)?\[x\]/gi, "<li>$1☑");
}
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);
});