Source: src/js/views/MarkdownEditorView.js

define(["underscore",
        "jquery",
        "backbone",
        "woofmark",
        "models/metadata/eml220/EMLText",
        "views/ImageUploaderView",
        "views/MarkdownView",
        "views/TableEditorView",
        "text!templates/markdownEditor.html"],
function(_, $, Backbone, Woofmark, EMLText, ImageUploader, MarkdownView, TableEditor, Template){

  /**
  * @class MarkdownEditorView
  * @classdesc A view of an HTML textarea with markdown editor UI and preview tab
  * @classcategory Views
  * @extends Backbone.View
  * @constructor
  */
  var MarkdownEditorView = Backbone.View.extend(
    /** @lends MarkdownEditorView.prototype */{

    /**
    * The type of View this is
    * @type {string}
    * @readonly
    */
    type: "MarkdownEditor",

    /**
    * The HTML classes to use for this view's element
    * @type {string}
    */
    className: "markdown-editor",

    /**
    * References to templates for this view. HTML files are converted to
    * Underscore.js templates
    * @type {Underscore.Template}
    */
    template: _.template(Template),

    /*
    * Markdown to insert into the textarea when the view is first rendered
    * @type {string}
    */
    // markdown: "",

    /**
    * EMLText model that contains a markdown attribute to edit. The markdown is
    * inserted into the textarea when the view is first rendered. If there's no markdown,
    * then the view looks for markdown from the markdownExample attribute in the model.
    * Note that if there are multiple markdown strings in the model, only the first
    * is rendered/edited.
    * @type {EMLText}
    */
    model: null,

    /**
    * The placeholder text to display in the textarea when it's empty
    * @type {string}
    */
    markdownPlaceholder: "",

    /**
    * The placeholder text to display in the preview area when there's no
    * markdown
    * @type {string}
    */
    previewPlaceholder: "",

    /**
    * Indicates whether or not to render a table of contents for the markdown
    * preview. If set to true, a table of contents will be shown in the preview
    * if there two or more top-level headers are rendered from the markdown.
    * @type {boolean}
    */
    showTOC: false,

    /**
     * The maximum height for uploaded image files. If a file is taller than this, it
     * will be resized without warning before being uploaded. If set to null,
     * the image won't be resized based on height (but might be depending on
     * maxImageWidth).
     * @type {number}
     * @default 1200
     * @since 2.15.0
     */
    maxImageHeight: 1200,

    /**
     * The maximum width for uploaded image files. If a file is wider than this, it
     * will be resized without warning before being uploaded. If set to null,
     * the image won't be resized based on width (but might be depending on
     * maxImageHeight).
     * @type {number}
     * @default 1200
     * @since 2.15.0
     */
    maxImageWidth: 1200,

    /**
    * A jQuery selector for the HTML textarea element that will contain the
    * markdown text.
    * @type {string}
    */
    textarea: ".markdown-textarea",

    /**
    * The events this view will listen to and the associated function to call.
    * @type {Object}
    */
    events: {
      "click #markdown-preview-link"   :    "previewMarkdown",
      "focusout .markdown-textarea"    :    "updateMarkdown"
    },

    /**
    * Initialize is executed when a new markdownEditor is created.
    * @param {Object} options - A literal object with options to pass to the view
    */
    initialize: function(options){
      if(typeof options !== "undefined"){
          this.model               =  options.model               || new EMLText();
          this.markdownPlaceholder =  options.markdownPlaceholder || "";
          this.previewPlaceholder  =  options.previewPlaceholder  || "";
          this.showTOC             =  options.showTOC             || false;
      }
    },

    /**
     * render - Renders the markdownEditor - add UI for adding and editing
     * markdown to a textarea
     */
    render: function(){

      try {

        // Save the view
        var view = this;

        // The markdown attribute in the model may be a string or an array of strings.
        // Although EML211 can comprise an array of markdown elements,
        // this view will only render/edit the first if there are multiple.
        var markdown = this.model.get("markdown");
        if(Array.isArray(markdown) && markdown.length){
          markdown = markdown[0]
        }
        if(!markdown || !markdown.length){
          markdown = this.model.get("markdownExample")
        }

        // Insert the template into the view
        this.$el.html(this.template({
          markdown: markdown || "",
          markdownPlaceholder: this.markdownPlaceholder || "",
          previewPlaceholder: this.previewPlaceholder || "",
          cid: this.cid
        })).data("view", this);

        // The textarea element that the markdown editor buttons & functions will edit
        var textarea = this.$el.find(this.textarea);

        if(textarea && textarea.length){
          textarea = textarea[0]
        }

        if(!textarea){
          console.log("error: the markdown editor view was not rendered because no textarea element was found.");
          return
        }

        // Set woofmark options. See https://github.com/bevacqua/woofmark
        var woofmarkOptions =
          {
              fencing: true,
              html: false,
              wysiwyg: false,
              defaultMode: "markdown",
              render: {
                // Hide buttons that switch between markdown, WYSIWYG, & HTML for now
                modes: function (button, id) {
                  button.remove();
                }
              }
          };

        // Set options for all the buttons that will be shown in the toolbar.
        // Buttons will be shown in the order they are listed.
        // Defaults from Woofmark will be used unless they are replaced here,
        // see: https://github.com/bevacqua/woofmark/blob/master/src/strings.js.
        // They key is the ID for the button.
        //    remove: if set to true, the button will be removed (use this to hide default woofmark buttons)
        //    icon: the name of the font awesome icon to show in the button. If no button or svg is set, the ID/key will be displayed instead.
        //    svg: svg code to show in the button. If no button or svg is set, the ID/key will be displayed instead.
        //    title: The title to show on hover
        //    function: The function to call when the button is pressed. It will be passed chunks, cmd, e (see Woofmark docs), plus the ID/key. Called with view as the this (context).
        //    shortcut: The keyboard shortcut to use for the button. This will only work if there is also a custom function set.
        //    insertDividerAfter: If set to true, a visual divider will be placed after this button.
        var buttonOptions = {
          // Default woofmark buttons to remove
          attachment: {
            remove: true
          },
          heading: {
            remove: true
          },
          hr: {
            remove: true
          },
          // Remove the default image uploader button so we can add our own that
          // uploads the image as a dataone object.
          image: {
            remove: true
          },
          // Default woofmark buttons to keep, with custom properties, + custom buttons
          bold: {
            icon: "bold"
          },
          italic: {
            icon: "italic",
          },
          strike: {
            title: "Strikethrough",
            icon: "strikethrough",
            shortcut: "Ctrl+Shift+X",
            function: view.strikethrough,
            insertDividerAfter: true
          },
          h1: {
            svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 2.9V0h7.8v2.9l-2 .5v7h7.7v-7l-2-.5V0h7.7v2.9l-2 .5v17.2l2 .5V24h-7.8v-2.9l2-.5V14H5.9v6.6l2 .5V24H0v-2.9l2-.5V3.4z"/><path fill-rule="nonzero" d="M24 16.4v-1.9h-1.4V5.8h-4.1v1.8H20v7h-1.4v1.8z"/></svg>`,
            title: "Top-level heading",
            function: view.addHeader
          },
          h2: {
            svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="nonzero" d="M23.2 17.3l.1-3.1h-2v1.3H18c.1-.7.8-1.5 2-2.2L22 12a4 4 0 001-1l.3-1.5c0-.9-.3-1.6-1-2.1-.6-.5-1.5-.8-2.6-.8-1.3 0-2.2.3-2.9 1-.6.6-1 1.5-1 2.8l2.2.1c0-.8.2-1.3.4-1.7.3-.3.6-.5 1.1-.5.4 0 .7.1.9.3.2.2.3.5.3.8 0 .4-.1.7-.4 1-.2.4-.6.7-1.1 1a17 17 0 00-2.1 1.9c-.5.5-.8 1-1 1.6-.3.6-.4 1.4-.4 2.3h7.6z"/><path d="M.5 4.6V2.3h6.3v2.3L5.2 5v5.6h6.2V5l-1.6-.4V2.3H16v2.3l-1.6.4v14l1.6.4v2.3H9.8v-2.3l1.6-.4v-5.3H5.2V19l1.6.4v2.3H.5v-2.3l1.6-.4V5z"/></svg>`,
            title: "Second-level heading",
            function: view.addHeader
          },
          h3: {
            svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="nonzero" d="M18.1 17.3c.7 0 1.4-.1 2-.4.6-.2 1-.6 1.4-1 .3-.6.5-1.1.5-1.7 0-1.2-.7-2-2-2.5 1-.5 1.6-1.2 1.6-2.2 0-.9-.4-1.6-1-2-.6-.6-1.5-.8-2.6-.8-1 0-1.9.3-2.5.8a3 3 0 00-1 2.2l2.1.1c0-.9.4-1.3 1.3-1.3.3 0 .6 0 .9.3.2.2.3.4.3.8s-.2.7-.5.9a3 3 0 01-1.5.3v1.9h.6c.5 0 1 0 1.2.3.3.3.5.7.5 1 0 .5-.2.8-.4 1.1-.3.3-.7.5-1.1.5-1 0-1.5-.6-1.5-1.8l-2.2.1c.1 2.3 1.4 3.4 4 3.4z"/><path d="M2 6.6V4.8h4.7v1.8l-1.2.3V11H10V6.9l-1.2-.3V4.8h4.6v1.8l-1.2.3V17l1.2.3v1.8H8.9v-1.8l1.2-.3v-3.9H5.5v4l1.2.2v1.8H2v-1.8l1.2-.3V7z"/></svg>`,
            title: "Tertiary heading",
            function: view.addHeader,
            insertDividerAfter: true
          },
          divider: {
            svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="22" height="3" x="1" y="10.58" fill-rule="evenodd"/></svg>`,
            function: view.addDivider,
            title: "Top-level heading",
            shortcut: "Ctrl+Enter"
          },
          ol: {
            title: "Ordered list",
            icon: "list-ol",
          },
          ul: {
            title: "Un-ordered list",
            icon: "list-ul",
            insertDividerAfter: true
          },
          quote: {
            icon: "quote-left",
          },
          code: {
            icon: "code",
            insertDividerAfter: true
          },
          link: {
            icon: "link"
          },
          d1Image: {
            icon: "picture",
            function: view.addMdImage,
            title: 'Image',
            // use Ctrl+G to overwrite the built-in woofmark image function
            shortcut: "Ctrl+G"
          },
          table: {
            icon: "table",
            function: view.addTable,
            insertDividerAfter: true
          }
        }

        var buttonKeys = Object.keys(buttonOptions);

        // PRE-RENDER WOOFMARK
        // Set titles on buttons before the Woofmark text editor is rendered.
        // This way we can use Woofmark's build in functionality to convert "Ctrl"
        // to "Cmd" symbol if user is on mac.
        _.each(buttonKeys, function(key, i) {
          var options = buttonOptions[key],
              title = options.title || key.charAt(0).toUpperCase() + key.slice(1),
              presetShortcut = "";

              if(Woofmark.strings.titles[key] && Woofmark.strings.titles[key].match(/Ctrl\+.*$/) ){
                presetShortcut = Woofmark.strings.titles[key].match(/Ctrl\+.*$/)[0];
              }

              var shortcut = options.shortcut || presetShortcut;

          if(title){
            Woofmark.strings.titles[key] = [title, shortcut].join(" ")
          }
          // So that we can identify buttons that we want to manipulate after
          // they are rendered, use the key as the button text for now.
          Woofmark.strings.buttons[key] = key;
        }, this);

        // RENDER WOOFMARK
        // Initialize the woofmark markdown editor
        this.markdownEditor = new Woofmark(textarea, woofmarkOptions);

        // POST-RENDER WOOFMARK
        // After the markdown editor is initialized..

        // Add custom functions
        _.each(buttonKeys, function(key, i) {
          var options = buttonOptions[key];
          if(options.function){
            // addCommandButton uses cmd, not ctrl
            var shortcut = ""
            if(options.shortcut){
              shortcut = options.shortcut.replace("Ctrl", "cmd");
            }
            view.markdownEditor.addCommandButton(key, shortcut, function(e, mode, chunks){
              options.function.call(view, e, mode, chunks, key)
            })
          }
        });

        // Modify the button order & appearance
        var buttonContainer = $(view.markdownEditor.textarea).parent().find(".wk-commands");
        var buttons = buttonContainer.find(".wk-command");
        _.each(buttonKeys, function(key, i) {
          // Re-order buttons based on the order in buttonOptions, and remove
          // any that are marked for removal
          var options = buttonOptions[key];
          var buttonEl = buttonContainer.find(".wk-command").filter(function (){
              return this.innerHTML == key;
          });
          if(options.remove !== true){
            // Add tooltip
            buttonEl.tooltip({
      				placement: "top",
      				delay: 500,
              trigger: "hover"
      			});
            // Add font awesome icon or SVG
            if(options.icon){
              buttonEl.html("<i class='icon-" +options.icon + "'></i>");
            } else if (options.svg){
              buttonEl.html(options.svg);
              buttonEl.find("svg").height("13px").width("auto");
            }
            buttonContainer.append(buttonEl);
            if(options.insertDividerAfter === true ){
              buttonContainer.append("<div class='wk-commands-divider'></div>")
            }
          } else {
            buttonEl.remove();
          }
        });

      } catch (e) {
        console.log(e);
        console.log("Failed to render the markdown editor UI, error message: " + e );
      }

    },

    /**
     * addHeader - description
     *
     * @param  {event}  e      is the original event object
     * @param  {string} mode   can be markdown, html, or wysiwyg
     * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
     * @param  {string} id     the ID of the function, set as they key in buttonOptions in the render function
     */
    addHeader: function(e, mode, chunks, id){

      // Get the header level from the ID
      var levelToCreate = parseInt(id.replace( /^\D+/g, ''));

      chunks.selection = chunks.selection
        .replace(/\s+/g, ' ')
        .replace(/(^\s+|\s+$)/g, '');

      if (!chunks.selection) {
        chunks.startTag = new Array(levelToCreate + 1).join('#') + ' ';
        chunks.selection = Woofmark.strings.placeholders.heading;
        chunks.endTag = '';
        chunks.skip({ before: 1, after: 1 });
        return;
      }

      chunks.findTags(/#+[ ]*/, /[ ]*#+/);

      if (/#+/.test(chunks.startTag)) {
        level = RegExp.lastMatch.length;
      }

      chunks.startTag = chunks.endTag = '';
      chunks.findTags(null, /\s?(-+|=+)/);

      if (/=+/.test(chunks.endTag)) {
        level = 1;
      }

      if (/-+/.test(chunks.endTag)) {
        level = 2;
      }

      chunks.startTag = chunks.endTag = '';
      chunks.skip({ before: 1, after: 1 });

      if (levelToCreate > 0) {
        chunks.startTag = new Array(levelToCreate + 1).join('#') + ' ';
      }

    },

    /**
     * addDivider - Add or remove a divider
     *
     * @param  {event} e      is the original event object
     * @param  {string} mode   can be markdown, html, or wysiwyg
     * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
     */
    addDivider: function(e, mode, chunks){

      // If the selection includes a divider, remove it
      var markdown = chunks.before + chunks.selection + chunks.after;
      var startSel = chunks.before.length;
      var endSel = startSel + chunks.selection.length + 1;
      var dividerRE = /(\r\n|\r|\n){2}-{3,}/gm;
      var dividerDeleted = false;
      while((match = dividerRE.exec(markdown)) !== null) {
        // +1 so that we don't delete the divider if selection is at the newlines before a divider
        var startDivider = match.index + 2;
        // +1 so that if the selection is at the end of a divider, it will still be deleted
        var endDivider = match.index + match[0].length + 1;
        if((endSel > startDivider && endSel <= endDivider) || (startSel < endDivider && startSel >= startDivider)){
          tableString = match[0];
          chunks.before = markdown.slice(0,startDivider-2) + "\n";
          chunks.selection = "";
          chunks.after = markdown.slice(endDivider);
          dividerDeleted = true;
          break;
        }
      }
      // If the divider was not deleted (therefore not detected), then add one
      if(!dividerDeleted){
        var dividerToAdd = "\n\n--------------------\n";
        if(/(\r\n|\r|\n){1}$/.test(chunks.before)){
          dividerToAdd = "\n--------------------\n";
        }
        chunks.before = chunks.before + dividerToAdd
      }
    },

    /**
     * addTable - Creates the UI for editing and adding tables to the textarea.
     * Detects whether the selection contained any part of a markdown table,
     * then opens a woofmark dialog box and inserts a table editor view. If a
     * table was selected, the table information is imported into the table
     * editor where the user can edit it. If no table was selected, then it
     * creates an empty table where the user can add data.
     *
     * @param  {event} e      is the original event object
     * @param  {string} mode   can be markdown, html, or wysiwyg
     * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
     */
    addTable: function(e, mode, chunks){

      // Use a modified version of the link dialog
      this.markdownEditor.showLinkDialog();

      // Select the image upload dialog elements so that we can customize it
      var dialog = $(".wk-prompt"),
          dialogContent = dialog.find(".wk-prompt-input-container"),
          dialogTitle = dialog.find(".wk-prompt-title"),
          dialogDescription = dialog.find(".wk-prompt-description"),
          dialogOkBtn = dialog.find(".wk-prompt-ok");

      // Detect whether the selection includes a markdown table.
      // If it does, ensure the complete table is selected, and save the
      // markdown table string segment to be parsed.
      var markdown = chunks.before + chunks.selection + chunks.after;
      var startSel = chunks.before.length;
      var endSel = startSel + chunks.selection.length + 1;
      var tableRE = /((\|[^|\r\n]*)+\|(\r?\n|\r)?)+((?:\s*\|\s*:?\s*[-=]+\s*:?\s*)+\|)(\n\s*(?:\|[^\n]+\|\r?\n?)*)?$/gm;
      // The regular expression used by showdown to detect tables:
      // var tableRE = /^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|ยจ0)/gm;
      var tables = markdown.match(tableRE);
      var tableString = "";
      while((match = tableRE.exec(markdown)) !== null) {
        var startTab = match.index;
        var endTab = match.index + match[0].length;
        if((endSel > startTab && endSel <= endTab) || (startSel < endTab && startSel >= startTab)){
          tableString = match[0];
          chunks.before = markdown.slice(0,startTab);
          chunks.selection = markdown.slice(startTab, endTab);
          chunks.after = markdown.slice(endTab);
          // Just use the first table match in which there is also at least partial selection
          break;
        }
      }

      // Clone the chunks object at this point in case the textarea loses focus
      // and the selection changes before the "ok" buttons is pressed
      const chunksClone = JSON.parse(JSON.stringify(chunks));

      // Add a table editor view.
      // Pass the parsesd markdown table, if there is one
      var tableEditor = new TableEditor({
        markdown: tableString
      });
      // Render the table editor
      tableEditor.render();
      // Add the rendered table editor to the dialog, update the dialog.
      dialogContent.html(tableEditor.el);
      dialogDescription.remove();
      dialogTitle.text("Insert Table");

      // Listen for when the OK button is clicked. Attach listener to the dialog
      // so that it's destroyed when the dialog is destroyed. It won't be called
      // if the user presses cancel.
      var view = this;
      dialogOkBtn.off('click');
      dialogOkBtn.on('click', function insertText(event) {
        var tableMarkdown = tableEditor.getMarkdown();
        view.markdownEditor.runCommand( function(chunks, mode){
          chunks.before = chunksClone.before;
          chunks.after = chunksClone.after;
          chunks.selection = tableMarkdown;
        });
      });


    },

    /**
     * addMdImage - The function that gets called when a user clicks the custom
     * add image button added to the markdown editor. It uses the UI created by
     * the ImageUploaderView to allow a user to select & upload an image to the
     * repository, and uses Woofmark's built-in add image functionality to
     * insert the correct markdown into the textarea. This function must be
     * called such that "this" is the markdownEditor view.
     */
    addMdImage: function() {

      try {
        var view = this;

        // Show woofmark's default image upload dialog, inserted at the end of body
        view.markdownEditor.showImageDialog();

        // Select the image upload dialog elements so that we can customize it
        var imageDialog = $(".wk-prompt"),
            imageDialogInput = imageDialog.find(".wk-prompt-input"),
            imageDialogDescription = imageDialog.find(".wk-prompt-description"),
            imageDialogOkBtn = imageDialog.find(".wk-prompt-ok"),
            // Save the inner HTML of the button for when we replace it
            // temporarily during image upload
            imageDialogOkBtnTxt = imageDialogOkBtn.html();

        // Create an ImageUploaderView and insert into this view.
        mdImageUploader = new ImageUploader({
          uploadInstructions: "Drag & drop an image here or click to upload",
          imageTagName:       "img",
          height:             "175",
          width:              "300",
          maxHeight:          view.maxImageHeight || null,
          maxWidth:           view.maxImageWidth || null
        });

        // Show when image is uploading; temporarily disable the OK button
        view.stopListening(mdImageUploader, "addedfile");
        view.listenTo(mdImageUploader, "addedfile", function(){
          // Disable the button during upload;
          imageDialogOkBtn.prop('disabled', true);
          imageDialogOkBtn.css({"opacity":"0.5", "cursor":"not-allowed"});
          imageDialogOkBtn.html(
            "<i class='icon-spinner icon-spin icon-large loading icon'></i> "+
            "Uploading..."
          );
        });

        // Update the image input URL when the image is successfully uploaded
        view.stopListening(mdImageUploader, "successSaving");
        view.listenTo(mdImageUploader, "successSaving", function(dataONEObject){

          //Execute the DataONEObject function that performs various functions after
          // a successful save
          dataONEObject.onSuccessfulSave();

          // Re-enable the button
          imageDialogOkBtn.prop('disabled', false);
          imageDialogOkBtn.html(imageDialogOkBtnTxt);
          imageDialogOkBtn.css({"opacity":"1", "cursor":"pointer"});

          // Get the uploaded image's url.
          //var url = dataONEObject.url();
          var url = "";

          if( MetacatUI.appModel.get("isCN") ){
            var sourceRepo;

            //Use the object service URL from the origin MN/datasource
            if( dataONEObject.get("datasource") ){
              sourceRepo = MetacatUI.nodeModel.getMember(dataONEObject.get("datasource"));
            }
              //Use the object service URL from the alt repo
            if( !sourceRepo ){
              sourceRepo = MetacatUI.appModel.getActiveAltRepo();
            }

            if( sourceRepo ){
              url = sourceRepo.objectServiceUrl;
            }
          }

          //If this MetacatUI deployment is pointing to a MN, use the meta service URL from the AppModel
          if( !url ){
            url = MetacatUI.appModel.get("objectServiceUrl") || MetacatUI.appModel.get("resolveServiceUrl");
          }

          url = url + dataONEObject.get("id");

          // Create title out of file name without extension.
          var title = dataONEObject.get("fileName");
          if(title && title.lastIndexOf(".") > 0) {
            title = title.substring(0, title.lastIndexOf("."));
          }

          // Add the url + title to the input
          imageDialogInput.val(url + ' "' + title + '"' );
        });

        // Clear the input when the image is removed
        view.stopListening(mdImageUploader, "removedfile");
        view.listenTo(mdImageUploader, "removedfile", function(){
          imageDialogInput.val("");
        });

        // Render the image uploader and insert it just after the upload
        // instructions in the image upload dialog box.
        mdImageUploader.render();
        // The instructions for uploading in image that displays in the prompt/dialog
        imageDialogDescription.text("Click or drag & drop to upload an image")
        $(mdImageUploader.el).insertAfter(imageDialogDescription);
        // Hide the input box for now, to keep the uploader simple
        imageDialogInput.hide();

      } catch (e) {
        console.log("Failed to load the UI for adding markdown images. Error: " + e);
      }

    },

    /**
     * strikethrough - Add or remove the markdown syntax for strike through to
     * the textarea. If there is text selected, then strike through formatting
     * will be added or removed from that selection. If no selection,
     * some placeholder text will be added surrounded by the strikethrough
     * delimiters.
     *
     * @param  {event} e      is the original event object
     * @param  {string} mode   can be markdown, html, or wysiwyg
     * @param  {object} chunks is a chunks object, describing the current state of the editor, see https://github.com/bevacqua/woofmark#chunks
     */
    strikethrough: function(e, mode, chunks){
      try {
        var markup = "~~";
        // exactly two tiles
        var tildes = '\\~{2}';
        // 2 tildes at the start of a string
        var rleading = /^(\~{2})/;
        // 2 tildes at the end of a string
        var rtrailing = /(\~{2}$)/;
        // 0-1 spaces at the end of a string
        var rtrailingspace = /(\s?)$/;
        // 2+ line breaks
        var rnewlines = /\n{2,}/g;
        // the text to add when no text is selected
        var placeholder = "strikethrough text";

        // Remove leading & trailing white space from selection
        // (but do not remove from the user's text)
        chunks.trim();
        // Replace 2+ consecutive line breaks with 1 linebreak, otherwise
        // strikethrough syntax is incorrect and won't render HTML as expected
        chunks.selection = chunks.selection.replace(rnewlines, '\n');

        // See if the text before or after already contains ~~ at the start/end
        var leadTildes = rtrailing.exec(chunks.before);
        var trailTildes = rleading.exec(chunks.after);
        // See if the selected text already contains ~~ at start or end
        var selectLeadTildes = rleading.exec(chunks.selection);
        var selectTrailTildes = rtrailing.exec(chunks.selection);

        // If the selection is already surrounded by ~~, remove them
        if(leadTildes && trailTildes){
          chunks.before = chunks.before.replace(rtrailing, "");
          chunks.after = chunks.after.replace(rleading, "");
        // If the selection starts & ends with ~~, remove them
        } else if (selectLeadTildes && selectTrailTildes) {
          chunks.selection = chunks.selection.replace(rleading, "");
          chunks.selection = chunks.selection.replace(rtrailing, "");
        // Otherwise, add a set of ~~
        } else {
          chunks.before = chunks.before + markup;
          chunks.after = markup + chunks.after;
          // Add the placeholder text if there was no selection
          if (chunks.selection.length <= 0){
            chunks.selection = placeholder
          }
        }
      } catch (e) {
        console.log("Failed to add or remove strikethrough formatting from markdown. Error: " + e );
      }
    },

    /**
     * updateMarkdown - Update the markdown attribute in this view using the
     * value of the markdown textarea
     */
    updateMarkdown: function(){
      try {

        newMarkdown = this.$(this.textarea).val();

        // The markdown attribute in the model may be a string or an array of strings.
        // Although EML211 can comprise an array of markdown elements,
        // this view will only edit the first if there are multiple.
        if(Array.isArray(this.model.get("markdown")) && markdown.length){
          // Clone then update arary before setting it on the model
          // so that the backbone "change" event is fired.
          // See https://stackoverflow.com/a/10240697
          var newMarkdownArray = _.clone(this.model.get("markdown"));
          newMarkdownArray[0] = newMarkdown;
          this.model.set("markdown", newMarkdownArray);
        } else {
          this.model.set("markdown", newMarkdown)
        }
      } catch (e) {
        console.log("Failed to the view's markdown attribute, error: " + e);
      }
    },

    /**
     * previewMarkdown - render the markdown preview.
     */
    previewMarkdown: function(){

      try{

        var markdown = this.model.get("markdown")
        if(Array.isArray(markdown)){
          markdown = markdown[0]
        }

        var markdownPreview = new MarkdownView({
          markdown: markdown || this.previewPlaceholder,
          showTOC: this.showTOC || false
        });

        // Render the preview
        markdownPreview.render();
        // Add the rendered markdown to the preview tab
        this.$("#markdown-preview-"+this.cid).html(markdownPreview.el);
      }

      catch(e){
        console.log("Failed to preview markdown content. Error message: " + e);
      }

    },

  });

  return MarkdownEditorView;

});