Source: src/js/views/MarkdownView.js

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<bibtex>" + this.citations + "</bibtex>";
                }

                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 ){
                $('<link/>', {
                   rel: 'stylesheet',
                   type: 'text/css',
                   href: path
                }).appendTo('head');
              }
            }

            addCSS("/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 <h.> 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;
});