define([ "jquery", "underscore", "backbone", "showdown", "text!templates/markdown.html", "text!templates/loading.html" ], function($, _, Backbone, showdown, markdownTemplate, LoadingTemplate ){ /** * @class MarkdownView * @classdesc A view of markdown content rendered into HTML with optional table of contents * @classcategory Views * @extends Backbone.View * @constructor */ var MarkdownView = Backbone.View.extend( /** @lends MarkdownView.prototype */{ /** * The HTML classes to use for this view's element * @type {string} */ className: "markdown", /** * The type of View this is * @type {string} * @readonly */ type: "markdown", /** * Renders the compiled template into HTML * @type {UnderscoreTemplate} */ template: _.template(markdownTemplate), loadingTemplate: _.template(LoadingTemplate), /** * Markdown to render into HTML * @type {string} */ markdown: "", /** * An array of literature cited * @type {Array} */ citations: [], /** * Indicates whether or not to render a table of contents for this view. * If set to true, a table of contents will be shown if there two or more * top-level headers are rendered from the markdown. * @type {boolean} */ showTOC: false, /** * The events this view will listen to and the associated function to * call. * @type {Object} */ events: { }, /** * Initialize is executed when a new MarkdownView is created. * @param {Object} options - A literal object with options to pass to the view */ initialize: function (options) { // highlightStyle = the name of the code syntax highlight style we // want to use for showdown's highlight extension. this.highlightStyle = "atom-one-light"; if(typeof options !== "undefined"){ this.markdown = options.markdown || ""; this.citations = options.citations || []; this.showTOC = options.showTOC || false; } }, /** * render - Renders the MarkdownView; converts markdown to HTML and * displays it. */ render: function() { // Show a loading message while we render the markdown to HTML this.$el.html(this.loadingTemplate({ msg: "Retrieving content..." })); // Once required extensions are tested for and loaded, convert and // append markdown this.stopListening(); this.listenTo(this, "requiredExtensionsLoaded", function(SDextensions){ var converter = new showdown.Converter({ metadata: true, simplifiedAutoLink:true, customizedHeaderId:true, parseImgDimension: true, tables:true, tablesHeaderId:true, strikethrough: true, tasklists: true, emoji: true, extensions: SDextensions }); // If there are citations in the markdown text, add it to the markdown // so it gets rendered. if( _.contains(SDextensions, "showdown-citation") && this.citations.length ){ // Put the bibtex into the markdown so it can be processed by // the showdown-citations extension. this.markdown = this.markdown + "\n" + this.citations + ""; } try{ // Use the Showdown converter to make HTML from the Markdown string htmlFromMD = converter.makeHtml( this.markdown ); } // If there was a Showdown error, show an error message instead of the Markdown preview. catch(e){ //Create a temporary div to hold the error message var errorMsgTempContainer = document.createElement("div"); //Create the error message MetacatUI.appView.showAlert("This content can't be displayed.", "alert-error", errorMsgTempContainer, { remove: false }); // Get the inner HTML of the temporary div htmlFromMD = errorMsgTempContainer.innerHTML; } this.$el.html(this.template({ markdown: htmlFromMD })); if( this.showTOC ){ this.listenToOnce(this, "TOCRendered", function(){ this.trigger("mdRendered"); this.postRender(); }); this.renderTOC(); } else { this.trigger("mdRendered"); this.postRender(); } }); // Detect which extensions we'll need this.listRequiredExtensions( this.markdown ); return this; }, postRender: function(){ if(this.tocView){ this.tocView.postRender(); } else { this.listenToOnce(this, "TOCRendered", function(){ this.tocView.postRender(); }); } }, /** * listRequiredExtensions - test which extensions are needed, then load * them * * @param {string} markdown - The markdown string before it's converted * into HTML */ listRequiredExtensions: function(markdown){ var view = this; // Given a path, check whether a CSS file was already added to the // head, and add it if not. Prevents adding the CSS file multiple // times if the view is loaded more than once. var addCSS = function(path){ if($("head link[href='" + path + "']").length <= 0 ){ $('', { rel: 'stylesheet', type: 'text/css', href: path }).appendTo('head'); } } addCSS(MetacatUI.root + "/components/showdown/extensions/showdown-katex/katex.min.css"); // SDextensions lists the desired order* of all potentailly required showdown extensions (* order matters! ) var SDextensions = ["xssfilter", "katex", "highlight", "docbook", "showdown-htags", "bootstrap", "footnotes", "showdown-citation", "showdown-images"]; var numTestsTodo = SDextensions.length; // Each time an extension is tested for (and loaded if required), // updateExtensionList is called. When all tests are completed // (numTestsTodo == 0), an event is triggered. When this event is // triggered, markdown is converted and appended (see render) var updateExtensionList = function(extensionName, required){ numTestsTodo = numTestsTodo - 1; if(required == false){ var n = SDextensions.indexOf(extensionName); SDextensions.splice(n, 1); } if(numTestsTodo == 0){ view.trigger("requiredExtensionsLoaded", SDextensions); } }; // ================================================================ // Regular expressions used to test whether showdown // extensions are required. // NOTE: These expressions test the *markdown* and *not* the HTML var regexHighlight = new RegExp("`.*`"), // too general? regexDocbook = new RegExp("<(title|citetitle|emphasis|para|ulink|literallayout|itemizedlist|orderedlist|listitem|subscript|superscript).*>"), regexFootnotes1 = /^\[\^([\d\w]+)\]:( |\n)((.+\n)*.+)$/m, regexFootnotes2 = /^\[\^([\d\w]+)\]:\s*((\n+(\s{2,4}|\t).+)+)$/m, regexFootnotes3 = /\[\^([\d\w]+)\]/m, // test for all of the math/katex delimiters regexKatex = new RegExp("\\$\\$.*\\$\\$|\\~.*\\~|\\$.*\\$|```asciimath.*```|```latex.*```"), regexCitation = /\[@.+\]/; // test for any tags regexHtags = new RegExp('#\\s'), regexImages = /!\[.*\]\(\S+\)/; // ================================================================ // Test for and load each as required each showdown extension // --- Test for XSS --- // // There is no test for the xss filter because it should always be // included. It's included via the updateExtensionList function for // consistency with the other, optional extensions. require(["showdownXssFilter"], function(showdownKatex){ updateExtensionList("xssfilter", required=true); }) // --- Test for katex --- // if( regexKatex.test(markdown) ){ require(["showdownKatex"], function(showdownKatex){ // custom config needed for katex var katex = showdownKatex({ delimiters: [ { left: "$", right: "$", display: false }, { left: "$$", right: "$$", display: false}, { left: '~', right: '~', display: false } ] }); // because custom config, register katex with showdown showdown.extension("katex", katex); updateExtensionList("katex", required=true); }); // css needed for katex addCSS(MetacatUI.root + "/components/showdown/extensions/showdown-katex/katex.min.css"); } else { updateExtensionList("katex", required=false); }; // --- Test for highlight --- // if( regexHighlight.test(markdown) ){ require(["showdownHighlight"], function(showdownHighlight){ updateExtensionList("highlight", required=true); }); // css needed for highlight addCSS(MetacatUI.root + "/components/showdown/extensions/showdown-highlight/styles/atom-one-light.css"); } else { updateExtensionList("highlight", required=false); }; // --- Test for docbooks --- // if( regexDocbook.test(markdown) ){ require(["showdownDocbook"], function(showdownDocbook){ updateExtensionList("docbook", required=true); }); } else { updateExtensionList("docbook", required=false); }; // --- Test for htag --- // if( regexHtags.test(markdown) ){ require(["showdownHtags"], function(showdownHtags){ updateExtensionList("showdown-htags", required=true); }); } else { updateExtensionList("showdown-htags", required=false); }; // --- Test for bootstrap --- // // The custom bootstrap library is small and only adds some classes // for tables and images, and maybe other HTML elements in the future. // Testing for tables in markdown using regular expressions isn't // straight forward. Better to just load this extension whether or // not it's required. require(["showdownBootstrap"], function(showdownBootstrap){ updateExtensionList("bootstrap", required=true); }); // --- Test for footnotes --- // if( regexFootnotes1.test(markdown) || regexFootnotes2.test(markdown) || regexFootnotes3.test(markdown) ){ require(["showdownFootnotes"], function(showdownFootnotes){ updateExtensionList("footnotes", required=true); }); } else { updateExtensionList("footnotes", required=false); }; // --- Test for citations --- // // showdownCitation throws error... if( regexCitation.test(markdown) ){ require(["showdownCitation"], function(showdownCitation){ updateExtensionList("showdown-citation", required=true); }); } else { updateExtensionList("showdown-citation", required=false); }; // --- Test for images --- // if( regexImages.test(markdown) ){ require(["showdownImages"], function(showdownImages){ updateExtensionList("showdown-images", required=true); }); } else { updateExtensionList("showdown-images", required=false); }; }, /** * Renders a table of contents (a TOCView) that links to different sections of the MarkdownView */ renderTOC: function(){ if(this.showTOC === false){ return } var view = this; require(["views/TOCView"], function(TOCView){ //Create a table of contents view view.tocView = new TOCView({ contentEl: view.el, className: "toc toc-view", addScrollspy: true, affix: true }); view.tocView.render(); // If more than one link was created in the TOCView, add it to this // view. Limit to `.desktop` items (i.e. exclude .mobile items) so // that the length isn't doubled if( view.tocView.$el.find(".desktop li").length > 1){ ($(view.tocView.el)).insertBefore(view.$el); // Make a two-column layout view.tocView.$el.addClass("span3"); view.$el.addClass("span9"); } view.trigger("TOCRendered"); }); }, /** * onClose - Close and destroy the view */ onClose: function() { // Remove for the DOM, stop listening this.remove(); // Remove appended html this.$el.html(""); } }); return MarkdownView; });