├── css └── jquery.pagedown-bootstrap.css ├── README.md ├── less └── jquery.pagedown-bootstrap.less ├── LICENSE.txt ├── js ├── jquery.pagedown-bootstrap.js ├── Markdown.Sanitizer.js ├── jquery.pagedown-bootstrap.combined.min.js ├── Markdown.Converter.js └── Markdown.Editor.js └── demo └── index.html /css/jquery.pagedown-bootstrap.css: -------------------------------------------------------------------------------- 1 | .wmd-panel{width:100%;} 2 | .wmd-input{height:300px;width:100%;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;} 3 | .wmd-preview{width:100%;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;} 4 | .wmd-panel .btn-toolbar{margin-bottom:0;padding:0 0 5px;width:100%;} 5 | .fa-header:before{content:'H';font-family:arial,helvetica,sans-serif;font-weight:bold;} 6 | .wmd-prompt-background{background-color:Black;} 7 | .wmd-prompt-dialog{border:1px solid #999999;background-color:#F5F5F5;} 8 | .wmd-prompt-dialog>div{font-size:0.8em;font-family:arial,helvetica,sans-serif;} 9 | .wmd-prompt-dialog>form>input[type="text"]{border:1px solid #999999;color:black;} 10 | .wmd-prompt-dialog>form>input[type="button"]{border:1px solid #888888;font-family:trebuchet MS,helvetica,sans-serif;font-size:0.8em;font-weight:bold;} 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PageDown-Bootstrap 2 | 3 | A **jQuery** plugin that dynamically generates a **WYSIWYG** editor for **Markdown** with integrated **Twitter Bootstrap** styling. 4 | 5 | ## Requirements 6 | * [jQuery 1.9+](http://jquery.com) (you know you should be keeping it up to date) 7 | * [Twitter Bootstrap 3.0+](http://getbootstrap.com/) 8 | * [FontAwesome 4.0+](http://fontawesome.io/) 9 | 10 | ## Releases 11 | 12 | ```1.0``` - April 22, 2013 13 | * Initial release 14 | 15 | ```1.1``` - December 14, 2013 16 | * Moved to support Bootstrap 3.x 17 | * Changed to FontAwesome as the icon provider 18 | 19 | ## Demo & Documentation 20 | [http://kevo.io/pagedown-bootstrap/](http://kevo.io/pagedown-bootstrap/) 21 | 22 | ## Thanks 23 | * Editor and converter written by the [Pagedown](http://code.google.com/p/pagedown/) project. 24 | * [Sam Willis](https://github.com/samwillis/pagedown-bootstrap) for his original Bootstrap integration 25 | * New icons based on [GLYPHICONS](http://glyphicons.com), [Julian Leiss' mini glyphs](http://dribbble.com/shots/365544-Mini-glyphs-12-px-Free-PSD), and the origional icons of [http://code.google.com/p/pagedown/](Pagedown). 26 | -------------------------------------------------------------------------------- /less/jquery.pagedown-bootstrap.less: -------------------------------------------------------------------------------- 1 | .wmd-panel { 2 | width: 100%; 3 | } 4 | 5 | .wmd-input { 6 | height: 300px; 7 | width: 100%; 8 | box-sizing: border-box; 9 | -webkit-box-sizing:border-box; 10 | -moz-box-sizing: border-box; 11 | -ms-box-sizing: border-box; 12 | } 13 | 14 | .wmd-preview { 15 | width: 100%; 16 | box-sizing: border-box; 17 | -webkit-box-sizing:border-box; 18 | -moz-box-sizing: border-box; 19 | -ms-box-sizing: border-box; 20 | } 21 | 22 | .wmd-panel .btn-toolbar { 23 | margin-bottom: 0; 24 | padding: 0 0 5px; 25 | width: 100%; 26 | } 27 | 28 | .fa-header:before { 29 | content: 'H'; 30 | font-family: arial, helvetica, sans-serif; 31 | font-weight: bold; 32 | } 33 | 34 | 35 | 36 | 37 | 38 | .wmd-prompt-background 39 | { 40 | background-color: Black; 41 | } 42 | 43 | .wmd-prompt-dialog 44 | { 45 | border: 1px solid #999999; 46 | background-color: #F5F5F5; 47 | } 48 | 49 | .wmd-prompt-dialog > div { 50 | font-size: 0.8em; 51 | font-family: arial, helvetica, sans-serif; 52 | } 53 | 54 | 55 | .wmd-prompt-dialog > form > input[type="text"] { 56 | border: 1px solid #999999; 57 | color: black; 58 | } 59 | 60 | .wmd-prompt-dialog > form > input[type="button"]{ 61 | border: 1px solid #888888; 62 | font-family: trebuchet MS, helvetica, sans-serif; 63 | font-size: 0.8em; 64 | font-weight: bold; 65 | } 66 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Pagedown Bootstrap 2 | ------------------- 3 | 4 | An extension of Pagedown which brings in Bootstrap and jQuery. Released 5 | under the MIT lciense (included below). 6 | 7 | Copyright (c) 2013 Kevin O'Connor 8 | 9 | 10 | Pagedown License 11 | ------------------- 12 | 13 | A javascript port of Markdown, as used on Stack Overflow 14 | and the rest of Stack Exchange network. 15 | 16 | Largely based on showdown.js by John Fraser (Attacklab). 17 | 18 | Original Markdown Copyright (c) 2004-2005 John Gruber 19 | 20 | 21 | 22 | Original Showdown code copyright (c) 2007 John Fraser 23 | 24 | Modifications and bugfixes (c) 2009 Dana Robinson 25 | Modifications and bugfixes (c) 2009-2011 Stack Exchange Inc. 26 | 27 | MIT License 28 | ------------------- 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in 38 | all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 46 | THE SOFTWARE. 47 | 48 | -------------------------------------------------------------------------------- /js/jquery.pagedown-bootstrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Pagedown Bootstrap 3 | * Author: Kevin O'Connor 4 | * Version: 1.0 5 | * 6 | * Copyright (c) 2013 Kevin O'Connor 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 9 | * associated documentation files (the "Software"), to deal in the Software without restriction, 10 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, 11 | * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 12 | * furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in all copies or 15 | * substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING 18 | * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 20 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | * 23 | */ 24 | 25 | (function( $ ){ 26 | 27 | $.fn.pagedownBootstrap = function( options ) { 28 | 29 | // Default settings 30 | var settings = $.extend( { 31 | 'sanitize' : true, 32 | 'help' : null, 33 | 'hooks' : Array() 34 | }, options); 35 | 36 | return this.each(function() { 37 | 38 | //Setup converter 39 | var converter = null; 40 | if(settings.sanitize) 41 | { 42 | converter = Markdown.getSanitizingConverter(); 43 | } else { 44 | converter = new Markdown.Converter() 45 | } 46 | 47 | //Register hooks 48 | for(var i in settings.hooks) 49 | { 50 | var hook = settings.hooks[i]; 51 | if(typeof hook !== 'object' || typeof hook.event === 'undefined' 52 | || typeof hook.callback !== 'function') 53 | { 54 | //A bad hook object was given 55 | continue; 56 | } 57 | 58 | converter.hooks.chain(hook.event, hook.callback); 59 | 60 | } 61 | 62 | //Try to find a valid id for this element 63 | var id = "wmd-input"; 64 | var idAppend = 0; 65 | while($("#"+id+"-"+idAppend.toString()).length > 0) 66 | { 67 | idAppend++; 68 | } 69 | 70 | //Assign the choosen id to the element 71 | $(this).attr('id', id+"-"+idAppend.toString()); 72 | 73 | //Wrap the element with the needed html 74 | $(this).wrap('
'); 75 | $(this).before('
'); 76 | $(this).after('
'); 77 | $(this).addClass('wmd-input'); 78 | 79 | //Setup help function 80 | help = null; 81 | if($.isFunction(settings.help)) 82 | { 83 | help = { handler: settings.help }; 84 | } 85 | 86 | //Setup editor 87 | var editor = new Markdown.Editor(converter, "-"+idAppend.toString(), help); 88 | editor.run(); 89 | 90 | }); 91 | 92 | }; 93 | })( jQuery ); 94 | -------------------------------------------------------------------------------- /js/Markdown.Sanitizer.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var output, Converter; 3 | if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module 4 | output = exports; 5 | Converter = require("./Markdown.Converter").Converter; 6 | } else { 7 | output = window.Markdown; 8 | Converter = output.Converter; 9 | } 10 | 11 | output.getSanitizingConverter = function () { 12 | var converter = new Converter(); 13 | converter.hooks.chain("postConversion", sanitizeHtml); 14 | converter.hooks.chain("postConversion", balanceTags); 15 | return converter; 16 | } 17 | 18 | function sanitizeHtml(html) { 19 | return html.replace(/<[^>]*>?/gi, sanitizeTag); 20 | } 21 | 22 | // (tags that can be opened/closed) | (tags that stand alone) 23 | var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i; 24 | // | 25 | var a_white = /^(]+")?\s?>|<\/a>)$/i; 26 | 27 | // ]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i; 29 | 30 | //
|
for twitter bootstrap 31 | var pre_white = /^(|<\/pre>)$/i; 32 | 33 | function sanitizeTag(tag) { 34 | if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(pre_white)) 35 | return tag; 36 | else 37 | return ""; 38 | } 39 | 40 | /// 41 | /// attempt to balance HTML tags in the html string 42 | /// by removing any unmatched opening or closing tags 43 | /// IMPORTANT: we *assume* HTML has *already* been 44 | /// sanitized and is safe/sane before balancing! 45 | /// 46 | /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593 47 | /// 48 | function balanceTags(html) { 49 | 50 | if (html == "") 51 | return ""; 52 | 53 | var re = /<\/?\w+[^>]*(\s|$|>)/g; 54 | // convert everything to lower case; this makes 55 | // our case insensitive comparisons easier 56 | var tags = html.toLowerCase().match(re); 57 | 58 | // no HTML tags present? nothing to do; exit now 59 | var tagcount = (tags || []).length; 60 | if (tagcount == 0) 61 | return html; 62 | 63 | var tagname, tag; 64 | var ignoredtags = "



  • "; 65 | var match; 66 | var tagpaired = []; 67 | var tagremove = []; 68 | var needsRemoval = false; 69 | 70 | // loop through matched tags in forward order 71 | for (var ctag = 0; ctag < tagcount; ctag++) { 72 | tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1"); 73 | // skip any already paired tags 74 | // and skip tags in our ignore list; assume they're self-closed 75 | if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1) 76 | continue; 77 | 78 | tag = tags[ctag]; 79 | match = -1; 80 | 81 | if (!/^<\//.test(tag)) { 82 | // this is an opening tag 83 | // search forwards (next tags), look for closing tags 84 | for (var ntag = ctag + 1; ntag < tagcount; ntag++) { 85 | if (!tagpaired[ntag] && tags[ntag] == "") { 86 | match = ntag; 87 | break; 88 | } 89 | } 90 | } 91 | 92 | if (match == -1) 93 | needsRemoval = tagremove[ctag] = true; // mark for removal 94 | else 95 | tagpaired[match] = true; // mark paired 96 | } 97 | 98 | if (!needsRemoval) 99 | return html; 100 | 101 | // delete all orphaned tags from the string 102 | 103 | var ctag = 0; 104 | html = html.replace(re, function (match) { 105 | var res = tagremove[ctag] ? "" : match; 106 | ctag++; 107 | return res; 108 | }); 109 | return html; 110 | } 111 | })(); 112 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PageDown-Bootstrap 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
    34 | 35 |
    36 |

    PageDown-Bootstrap

    37 |

    38 | An extension of Pagedown to neatly integrate into Twitter Bootstrap for a WYSIWYG Markdown editor. This project is a fork of the work of Sam Willis. 39 |

    40 |

    41 | This project is hosted on GitHub where issues can be reported. It is released under the MIT License. 42 |

    43 |

    44 | 45 | Fork on GitHub 46 | 47 |

    48 |
    49 | 50 |
    51 |

    Requirements

    52 | 57 | 58 |
    59 | 60 |

    Basic usage

    61 | 62 | 63 |
    64 | 65 |

    Options

    66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 95 | 96 | 97 |
    NameTypeDefaultOptions/Description
    sanitizebooleantruetrue or false, enables or disables sanitization of HTML
    helpfunctionnullA function to be called when the help button it clicked. Null precludes the rendering of the help button.
    hooksarray[] 93 | An array of objects that define hooks for the converter. Each object should define a string, 'event', and a function, 'callback'. A full list of events can be found here. 94 |
    98 | 99 |
    100 | 101 |

    Styling

    102 |

    103 | All of the created elements are wrapped in nice helpful HTML tags as defined: 104 |

    105 |
    106 |
    div.wmd-panel
    107 |
    A wrapper around all of the elements of the textarea, button bar, and preview
    108 |
    div.wmd-button-bar
    109 |
    A wrapper that contains the editor's button bar.
    110 |
    textarea.wmd-input
    111 |
    The textarea that has been transformed into an editor
    112 |
    div.wmd-preview
    113 |
    A wrapper for the preview generated by the editor
    114 |
    115 | 116 |
    117 | 118 |

    Demo

    119 | 120 |

    1. This is the basic editor

    121 | 129 |

    Code:

    130 | 131 | 132 |
    133 | 134 |

    2. This is a custom editor

    135 | 136 | 152 |

    Code:

    153 | 154 |
    155 | 156 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /js/jquery.pagedown-bootstrap.combined.min.js: -------------------------------------------------------------------------------- 1 | var Markdown;if(typeof exports==="object"&&typeof require==="function")Markdown=exports;else Markdown={};(function(){function e(e){return e}function t(e){return false}function n(){}function r(){}n.prototype={chain:function(t,n){var r=this[t];if(!r)throw new Error("unknown hook "+t);if(r===e)this[t]=n;else this[t]=function(e){return n(r(e))}},set:function(e,t){if(!this[e])throw new Error("unknown hook "+e);this[e]=t},addNoop:function(t){this[t]=e},addFalse:function(e){this[e]=t}};Markdown.HookCollection=n;r.prototype={set:function(e,t){this["s_"+e]=t},get:function(e){return this["s_"+e]}};Markdown.Converter=function(){function u(e){e=e.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm,function(e,n,r,s,o,u){n=n.toLowerCase();t.set(n,A(r));if(o){return s}else if(u){i.set(n,u.replace(/"/g,"""))}return""});return e}function a(e){var t="p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del";var n="p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math";e=e.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,f);e=e.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm,f);e=e.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,f);e=e.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g,f);e=e.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,f);return e}function f(e,t){var n=t;n=n.replace(/^\n+/,"");n=n.replace(/\n+$/g,"");n="\n\n~K"+(s.push(n)-1)+"K\n\n";return n}function l(e,t){e=y(e);var n="
    \n";e=e.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,n);e=e.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm,n);e=e.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm,n);e=b(e);e=S(e);e=k(e);e=a(e);e=L(e,t);return e}function c(e){e=T(e);e=h(e);e=O(e);e=v(e);e=p(e);e=M(e);e=e.replace(/~P/g,"://");e=A(e);e=C(e);e=e.replace(/ +\n/g,"
    \n");return e}function h(e){var t=/(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi;e=e.replace(t,function(e){var t=e.replace(/(.)<\/?code>(?=.)/g,"$1`");t=j(t,e.charAt(1)=="!"?"\\`*_/":"\\`*_");return t});return e}function p(e){e=e.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,d);e=e.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,d);e=e.replace(/(\[([^\[\]]+)\])()()()()()/g,d);return e}function d(e,n,r,s,o,u,a,f){if(f==undefined)f="";var l=n;var c=r.replace(/:\/\//g,"~P");var h=s.toLowerCase();var p=o;var d=f;if(p==""){if(h==""){h=c.toLowerCase().replace(/ ?\n/g," ")}p="#"+h;if(t.get(h)!=undefined){p=t.get(h);if(i.get(h)!=undefined){d=i.get(h)}}else{if(l.search(/\(\s*\)$/m)>-1){p=""}else{return l}}}p=B(p);p=j(p,"*_");var v='";return v}function v(e){e=e.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,g);e=e.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,g);return e}function m(e){return e.replace(/>/g,">").replace(/"+c(t)+"\n\n"});e=e.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,function(e,t){return"

    "+c(t)+"

    \n\n"});e=e.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,function(e,t,n){var r=t.length;return""+c(n)+"\n\n"});return e}function b(e){e+="~0";var t=/^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;if(o){e=e.replace(t,function(e,t,n){var r=t;var i=n.search(/[*+-]/g)>-1?"ul":"ol";var s=E(r,i);s=s.replace(/\s+$/,"");s="<"+i+">"+s+"\n";return s})}else{t=/(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g;e=e.replace(t,function(e,t,n,r){var i=t;var s=n;var o=r.search(/[*+-]/g)>-1?"ul":"ol";var u=E(s,o);u=i+"<"+o+">\n"+u+"\n";return u})}e=e.replace(/~0/,"");return e}function E(e,t){o++;e=e.replace(/\n{2,}$/,"\n");e+="~0";var n=w[t];var r=new RegExp("(^[ \\t]*)("+n+")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1("+n+")[ \\t]+))","gm");var i=false;e=e.replace(r,function(e,t,n,r){var s=r;var o=t;var u=/\n\n$/.test(s);var a=u||s.search(/\n{2,}/)>-1;if(a||i){s=l(D(s),true)}else{s=b(D(s));s=s.replace(/\n$/,"");s=c(s)}i=u;return"
  • "+s+"
  • \n"});e=e.replace(/~0/g,"");o--;return e}function S(e){e+="~0";e=e.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,function(e,t,n){var r=t;var i=n;r=N(D(r));r=P(r);r=r.replace(/^\n+/g,"");r=r.replace(/\n+$/g,"");r='
    '+r+"\n
    ";return"\n\n"+r+"\n\n"+i});e=e.replace(/~0/,"");return e}function x(e){e=e.replace(/(^\n+|\n+$)/g,"");return"\n\n~K"+(s.push(e)-1)+"K\n\n"}function T(e){e=e.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(e,t,n,r,i){var s=r;s=s.replace(/^([ \t]*)/g,"");s=s.replace(/[ \t]*$/g,"");s=N(s);s=s.replace(/:\/\//g,"~P");return t+""+s+""});return e}function N(e){e=e.replace(/&/g,"&");e=e.replace(//g,">");e=j(e,"*_{}[]\\",false);return e}function C(e){e=e.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g,"$1$3$4");e=e.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g,"$1$3$4");return e}function k(e){e=e.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,function(e,t){var n=t;n=n.replace(/^[ \t]*>[ \t]?/gm,"~0");n=n.replace(/~0/g,"");n=n.replace(/^[ \t]+$/gm,"");n=l(n);n=n.replace(/(^|\n)/g,"$1 ");n=n.replace(/(\s*
    [^\r]+?<\/pre>)/gm,function(e,t){var n=t;n=n.replace(/^  /mg,"~0");n=n.replace(/~0/g,"");return n});return x("
    \n"+n+"\n
    ")});return e}function L(e,t){e=e.replace(/^\n+/g,"");e=e.replace(/\n+$/g,"");var n=e.split(/\n{2,}/g);var r=[];var i=/~K(\d+)K/;var o=n.length;for(var u=0;u");a+="

    ";r.push(a)}}if(!t){o=r.length;for(var u=0;u#+-.!])/g,F);return e}function M(t){t=t.replace(/(^|\s)(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\]])($|\W)/gi,"$1<$2$3>$4");var n=function(t,n){return'
    '+e.plainLinkText(n)+""};t=t.replace(/<((https?|ftp):[^'">\s]+)>/gi,n);var r=function(t,n){var r="mailto:";var i;var s;if(n.substring(0,r.length)!=r){i=r+n;s=n}else{i=n;s=n.substring(r.length,n.length)}return''+e.plainLinkText(s)+""};t=t.replace(/<((?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+))>/gi,r);return t}function _(e){e=e.replace(/~E(\d+)E/g,function(e,t){var n=parseInt(t);return String.fromCharCode(n)});return e}function D(e){e=e.replace(/^(\t|[ ]{1,4})/gm,"~0");e=e.replace(/~0/g,"");return e}function P(e){if(!/\t/.test(e))return e;var t=[" "," "," "," "],n=0,r;return e.replace(/[\n\t]/g,function(e,i){if(e==="\n"){n=i+1;return e}r=(i-n)%4;n=i+1;return t[r]})}function B(e){if(!e)return"";var t=e.length;return e.replace(H,function(n,r){if(n=="~D")return"%24";if(n==":"){if(r==t-1||/[0-9\/]/.test(e.charAt(r+1)))return":";if(e.substring(0,"mailto:".length)==="mailto:")return":";if(e.substring(0,"magnet:".length)==="magnet:")return":"}return"%"+n.charCodeAt(0).toString(16)})}function j(e,t,n){var r="(["+t.replace(/([\[\]\\])/g,"\\$1")+"])";if(n){r="\\\\"+r}var i=new RegExp(r,"g");e=e.replace(i,F);return e}function F(e,t){var n=t.charCodeAt(0);return"~E"+n+"E"}var e=this.hooks=new n;e.addNoop("plainLinkText");e.addNoop("preConversion");e.addNoop("postConversion");var t;var i;var s;var o;this.makeHtml=function(n){if(t)throw new Error("Recursive call to converter.makeHtml");t=new r;i=new r;s=[];o=0;n=e.preConversion(n);n=n.replace(/~/g,"~T");n=n.replace(/\$/g,"~D");n=n.replace(/\r\n/g,"\n");n=n.replace(/\r/g,"\n");n="\n\n"+n+"\n\n";n=P(n);n=n.replace(/^[ \t]+$/mg,"");n=a(n);n=u(n);n=l(n);n=_(n);n=n.replace(/~D/g,"$$");n=n.replace(/~T/g,"~");n=e.postConversion(n);s=i=t=null;return n};var w={ol:"\\d+[.]",ul:"[*+-]"};var H=/(?:["'*()[\]:]|~D)/g}})();(function(){function p(){}function d(e){this.buttonBar=r.getElementById("wmd-button-bar"+e);this.preview=r.getElementById("wmd-preview"+e);this.input=r.getElementById("wmd-input"+e)}function v(t,n){var r=this;var i=[];var s=0;var o="none";var a;var f;var l;var c=function(e,t){if(o!=e){o=e;if(!t){p()}}if(!u.isIE||o!="moving"){f=setTimeout(h,1)}else{l=null}};var h=function(e){l=new m(n,e);f=undefined};this.setCommandMode=function(){o="command";p();f=setTimeout(h,0)};this.canUndo=function(){return s>1};this.canRedo=function(){if(i[s+1]){return true}return false};this.undo=function(){if(r.canUndo()){if(a){a.restore();a=null}else{i[s]=new m(n);i[--s].restore();if(t){t()}}}o="none";n.input.focus();h()};this.redo=function(){if(r.canRedo()){i[++s].restore();if(t){t()}}o="none";n.input.focus();h()};var p=function(){var e=l||new m(n);if(!e){return false}if(o=="moving"){if(!a){a=e}return}if(a){if(i[s-1].text!=a.text){i[s++]=a}a=null}i[s++]=e;i[s+1]=null;if(t){t()}};var d=function(e){var t=false;if(e.ctrlKey||e.metaKey){var n=e.charCode||e.keyCode;var i=String.fromCharCode(n);switch(i){case"y":r.redo();t=true;break;case"z":if(!e.shiftKey){r.undo()}else{r.redo()}t=true;break}}if(t){if(e.preventDefault){e.preventDefault()}if(window.event){window.event.returnValue=false}return}};var v=function(e){if(!e.ctrlKey&&!e.metaKey){var t=e.keyCode;if(t>=33&&t<=40||t>=63232&&t<=63235){c("moving")}else if(t==8||t==46||t==127){c("deleting")}else if(t==13){c("newlines")}else if(t==27){c("escape")}else if((t<16||t>20)&&t!=91){c("typing")}}};var g=function(){e.addEvent(n.input,"keypress",function(e){if((e.ctrlKey||e.metaKey)&&(e.keyCode==89||e.keyCode==90)){e.preventDefault()}});var t=function(){if(u.isIE||l&&l.text!=n.input.value){if(f==undefined){o="paste";p();h()}}};e.addEvent(n.input,"keydown",d);e.addEvent(n.input,"keydown",v);e.addEvent(n.input,"mousedown",function(){c("moving")});n.input.onpaste=t;n.input.ondrop=t};var y=function(){g();h(true);p()};y()}function m(t,n){var i=this;var s=t.input;this.init=function(){if(!e.isVisible(s)){return}if(!n&&r.activeElement&&r.activeElement!==s){return}this.setInputAreaSelectionStartEnd();this.scrollTop=s.scrollTop;if(!this.text&&s.selectionStart||s.selectionStart===0){this.text=s.value}};this.setInputAreaSelection=function(){if(!e.isVisible(s)){return}if(s.selectionStart!==undefined&&!u.isOpera){s.focus();s.selectionStart=i.start;s.selectionEnd=i.end;s.scrollTop=i.scrollTop}else if(r.selection){if(r.activeElement&&r.activeElement!==s){return}s.focus();var t=s.createTextRange();t.moveStart("character",-s.value.length);t.moveEnd("character",-s.value.length);t.moveEnd("character",i.end);t.moveStart("character",i.start);t.select()}};this.setInputAreaSelectionStartEnd=function(){if(!t.ieCachedRange&&(s.selectionStart||s.selectionStart===0)){i.start=s.selectionStart;i.end=s.selectionEnd}else if(r.selection){i.text=e.fixEolChars(s.value);var n=t.ieCachedRange||r.selection.createRange();var o=e.fixEolChars(n.text);var u="";var a=u+o+u;n.text=a;var f=e.fixEolChars(s.value);n.moveStart("character",-a.length);n.text=o;i.start=f.indexOf(u);i.end=f.lastIndexOf(u)-u.length;var l=i.text.length-e.fixEolChars(s.value).length;if(l){n.moveStart("character",-o.length);while(l--){o+="\n";i.end+=1}n.text=o}if(t.ieCachedRange)i.scrollTop=t.ieCachedScrollTop;t.ieCachedRange=null;this.setInputAreaSelection()}};this.restore=function(){if(i.text!=undefined&&i.text!=s.value){s.value=i.text}this.setInputAreaSelection();s.scrollTop=i.scrollTop};this.getChunks=function(){var t=new p;t.before=e.fixEolChars(i.text.substring(0,i.start));t.startTag="";t.selection=e.fixEolChars(i.text.substring(i.start,i.end));t.endTag="";t.after=e.fixEolChars(i.text.substring(i.end));t.scrollTop=i.scrollTop;return t};this.setChunks=function(e){e.before=e.before+e.startTag;e.after=e.endTag+e.after;this.start=e.before.length;this.end=e.before.length+e.selection.length;this.text=e.before+e.selection+e.after;this.scrollTop=e.scrollTop};this.init()}function g(n,i,s){var o=this;var a;var f;var l;var c=3e3;var h="delayed";var p=function(t,n){e.addEvent(t,"input",n);t.onpaste=n;t.ondrop=n;e.addEvent(t,"keypress",n);e.addEvent(t,"keydown",n)};var d=function(){var e=0;if(window.innerHeight){e=window.pageYOffset}else if(r.documentElement&&r.documentElement.scrollTop){e=r.documentElement.scrollTop}else if(r.body){e=r.body.scrollTop}return e};var v=function(){if(!i.preview)return;var e=i.input.value;if(e&&e==l){return}else{l=e}var t=(new Date).getTime();e=n.makeHtml(e);var r=(new Date).getTime();f=r-t;T(e)};var m=function(){if(a){clearTimeout(a);a=undefined}if(h!=="manual"){var e=0;if(h==="delayed"){e=f}if(e>c){e=c}a=setTimeout(v,e)}};var g=function(e){if(e.scrollHeight<=e.clientHeight){return 1}return e.scrollTop/(e.scrollHeight-e.clientHeight)};var y=function(){if(i.preview){i.preview.scrollTop=(i.preview.scrollHeight-i.preview.clientHeight)*g(i.preview)}};this.refresh=function(e){if(e){l="";v()}else{m()}};this.processingTime=function(){return f};var b=true;var w=function(e){var t=i.preview;var n=t.parentNode;var r=t.nextSibling;n.removeChild(t);t.innerHTML=e;if(!r)n.appendChild(t);else n.insertBefore(t,r)};var E=function(e){i.preview.innerHTML=e};var S;var x=function(e){if(S)return S(e);try{E(e);S=E}catch(t){S=w;S(e)}};var T=function(e){var n=t.getTop(i.input)-d();if(i.preview){x(e);s()}y();if(b){b=false;return}var r=t.getTop(i.input)-d();if(u.isIE){setTimeout(function(){window.scrollBy(0,r-n)},0)}else{window.scrollBy(0,r-n)}};var N=function(){p(i.input,m);v();if(i.preview){i.preview.scrollTop=0}};N()}function y(t,n,r,i,o,a){function p(e){f.focus();if(e.textOp){if(r){r.setCommandMode()}var t=new m(n);if(!t){return}var s=t.getChunks();var o=function(){f.focus();if(s){t.setChunks(s)}t.restore();i.refresh()};var u=e.textOp(s,o);if(!u){o()}}if(e.execute){e.execute(r)}}function d(e,t){if(t){e.disabled=false;if(!e.isHelp){e.onclick=function(){if(this.onmouseout){this.onmouseout()}p(this);return false}}}else{e.disabled=true}}function v(e){if(typeof e==="string")e=o[e];return function(){e.apply(o,arguments)}}function g(){var e=n.buttonBar;var r=document.createElement("div");r.id="wmd-button-row"+t;r.className="btn-toolbar";r=e.appendChild(r);var i=function(e,n,i,s,o){var u=document.createElement("button");u.className="btn btn-default";var a=document.createElement("i");a.className=i;u.id=e+t;u.appendChild(a);u.title=n;$(u).tooltip({placement:"bottom",container:"body"});if(s)u.textOp=s;d(u,true);if(o){o.appendChild(u)}else{r.appendChild(u)}return u};var o=function(e){var n=document.createElement("div");n.className="btn-group wmd-button-group"+e;n.id="wmd-button-group"+e+t;r.appendChild(n);return n};group1=o(1);l.bold=i("wmd-bold-button","Bold - Ctrl+B","fa fa-bold",v("doBold"),group1);l.italic=i("wmd-italic-button","Italic - Ctrl+I","fa fa-italic",v("doItalic"),group1);group2=o(2);l.link=i("wmd-link-button","Link - Ctrl+L","fa fa-link",v(function(e,t){return this.doLinkOrImage(e,t,false)}),group2);l.quote=i("wmd-quote-button","Blockquote - Ctrl+Q","fa fa-quote-left",v("doBlockquote"),group2);l.code=i("wmd-code-button","Code Sample - Ctrl+K","fa fa-code",v("doCode"),group2);l.image=i("wmd-image-button","Image - Ctrl+G","fa fa-picture-o",v(function(e,t){return this.doLinkOrImage(e,t,true)}),group2);group3=o(3);l.olist=i("wmd-olist-button","Numbered List - Ctrl+O","fa fa-list-ol",v(function(e,t){this.doList(e,t,true)}),group3);l.ulist=i("wmd-ulist-button","Bulleted List - Ctrl+U","fa fa-list-ul",v(function(e,t){this.doList(e,t,false)}),group3);l.heading=i("wmd-heading-button","Heading - Ctrl+H","fa fa-header",v("doHeading"),group3);l.hr=i("wmd-hr-button","Horizontal Rule - Ctrl+R","fa fa-ellipsis-h",v("doHorizontalRule"),group3);group4=o(4);l.undo=i("wmd-undo-button","Undo - Ctrl+Z","fa fa-undo",null,group4);l.undo.execute=function(e){if(e)e.undo()};var u=/win/.test(s.platform.toLowerCase())?"Redo - Ctrl+Y":"Redo - Ctrl+Shift+Z";l.redo=i("wmd-redo-button",u,"fa fa-rotate-right",null,group4);l.redo.execute=function(e){if(e)e.redo()};if(a){group5=o(5);group5.className=group5.className+" pull-right";var f=document.createElement("button");var c=document.createElement("i");c.className="fa fa-question";f.appendChild(c);f.className="btn";f.id="wmd-help-button"+t;f.isHelp=true;f.title=a.title||h;$(f).tooltip({placement:"bottom",container:"body"});f.onclick=a.handler;d(f,true);group5.appendChild(f);l.help=f}y()}function y(){if(r){d(l.undo,r.canUndo());d(l.redo,r.canRedo())}}var f=n.input,l={};g();var c="keydown";if(u.isOpera){c="keypress"}e.addEvent(f,c,function(e){if((e.ctrlKey||e.metaKey)&&!e.altKey&&!e.shiftKey){var t=e.charCode||e.keyCode;var n=String.fromCharCode(t).toLowerCase();switch(n){case"b":p(l.bold);break;case"i":p(l.italic);break;case"l":p(l.link);break;case"q":p(l.quote);break;case"k":p(l.code);break;case"g":p(l.image);break;case"o":p(l.olist);break;case"u":p(l.ulist);break;case"h":p(l.heading);break;case"r":p(l.hr);break;case"y":p(l.redo);break;case"z":if(e.shiftKey){p(l.redo)}else{p(l.undo)}break;default:return}if(e.preventDefault){e.preventDefault()}if(window.event){window.event.returnValue=false}}});e.addEvent(f,"keyup",function(e){if(e.shiftKey&&!e.ctrlKey&&!e.metaKey){var t=e.charCode||e.keyCode;if(t===13){var n={};n.textOp=v("doAutoindent");p(n)}}});if(u.isIE){e.addEvent(f,"keydown",function(e){var t=e.keyCode;if(t===27){return false}})}this.setUndoRedoButtonStates=y}function b(e){this.hooks=e}function E(e){return e.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/,function(e,t,n){t=t.replace(/\?.*$/,function(e){return e.replace(/\+/g," ")});t=decodeURIComponent(t);t=encodeURI(t).replace(/'/g,"%27").replace(/\(/g,"%28").replace(/\)/g,"%29");t=t.replace(/\?.*$/,function(e){return e.replace(/\+/g,"%2b")});if(n){n=n.trim?n.trim():n.replace(/^\s*/,"").replace(/\s*$/,"");n=$.trim(n).replace(/"/g,"quot;").replace(/\(/g,"(").replace(/\)/g,")").replace(//g,">")}return n?t+' "'+n+'"':t})}var e={},t={},n={},r=window.document,i=window.RegExp,s=window.navigator,o={lineLength:72},u={isIE:/msie/.test(s.userAgent.toLowerCase()),isIE_5or6:/msie 6/.test(s.userAgent.toLowerCase())||/msie 5/.test(s.userAgent.toLowerCase()),isOpera:/opera/.test(s.userAgent.toLowerCase())};var a='http://example.com/ "optional title"';var f='http://example.com/images/diagram.jpg "optional title"';var l="http://";var c="http://";var h="Markdown Editing Help";Markdown.Editor=function(e,t,n){t=t||"";var i=this.hooks=new Markdown.HookCollection;i.addNoop("onPreviewRefresh");i.addNoop("postBlockquoteCreation");i.addFalse("insertImageDialog");this.getConverter=function(){return e};var s=this,o;this.run=function(){if(o)return;o=new d(t);var u=new b(i);var a=new g(e,o,function(){i.onPreviewRefresh()});var f,l;if(!/\?noundo/.test(r.location.href)){f=new v(function(){a.refresh();if(l)l.setUndoRedoButtonStates()},o);this.textOperation=function(e){f.setCommandMode();e();s.refreshPreview()}}l=new y(t,o,f,a,u,n);l.setUndoRedoButtonStates();var c=s.refreshPreview=function(){a.refresh(true)};c()}};p.prototype.findTags=function(t,n){var r=this;var i;if(t){i=e.extendRegExp(t,"","$");this.before=this.before.replace(i,function(e){r.startTag=r.startTag+e;return""});i=e.extendRegExp(t,"^","");this.selection=this.selection.replace(i,function(e){r.startTag=r.startTag+e;return""})}if(n){i=e.extendRegExp(n,"","$");this.selection=this.selection.replace(i,function(e){r.endTag=e+r.endTag;return""});i=e.extendRegExp(n,"^","");this.after=this.after.replace(i,function(e){r.endTag=e+r.endTag;return""})}};p.prototype.trimWhitespace=function(e){var t,n,r=this;if(e){t=n=""}else{t=function(e){r.before+=e;return""};n=function(e){r.after=e+r.after;return""}}this.selection=this.selection.replace(/^(\s*)/,t).replace(/(\s*)$/,n)};p.prototype.skipLines=function(e,t,n){if(e===undefined){e=1}if(t===undefined){t=1}e++;t++;var r;var s;if(navigator.userAgent.match(/Chrome/)){"X".match(/()./)}this.selection=this.selection.replace(/(^\n*)/,"");this.startTag=this.startTag+i.$1;this.selection=this.selection.replace(/(\n*$)/,"");this.endTag=this.endTag+i.$1;this.startTag=this.startTag.replace(/(^\n*)/,"");this.before=this.before+i.$1;this.endTag=this.endTag.replace(/(\n*$)/,"");this.after=this.after+i.$1;if(this.before){r=s="";while(e--){r+="\\n?";s+="\n"}if(n){r="\\n*"}this.before=this.before.replace(new i(r+"$",""),s)}if(this.after){r=s="";while(t--){r+="\\n?";s+="\n"}if(n){r="\\n*"}this.after=this.after.replace(new i(r,""),s)}};e.isVisible=function(e){if(window.getComputedStyle){return window.getComputedStyle(e,null).getPropertyValue("display")!=="none"}else if(e.currentStyle){return e.currentStyle["display"]!=="none"}};e.addEvent=function(e,t,n){if(e.attachEvent){e.attachEvent("on"+t,n)}else{e.addEventListener(t,n,false)}};e.removeEvent=function(e,t,n){if(e.detachEvent){e.detachEvent("on"+t,n)}else{e.removeEventListener(t,n,false)}};e.fixEolChars=function(e){e=e.replace(/\r\n/g,"\n");e=e.replace(/\r/g,"\n");return e};e.extendRegExp=function(e,t,n){if(t===null||t===undefined){t=""}if(n===null||n===undefined){n=""}var r=e.toString();var s;r=r.replace(/\/([gim]*)$/,function(e,t){s=t;return""});r=r.replace(/(^\/|\/$)/g,"");r=t+r+n;return new i(r,s)};t.getTop=function(e,t){var n=e.offsetTop;if(!t){while(e=e.offsetParent){n+=e.offsetTop}}return n};t.getHeight=function(e){return e.offsetHeight||e.scrollHeight};t.getWidth=function(e){return e.offsetWidth||e.scrollWidth};t.getPageSize=function(){var e,t;var n,i;if(self.innerHeight&&self.scrollMaxY){e=r.body.scrollWidth;t=self.innerHeight+self.scrollMaxY}else if(r.body.scrollHeight>r.body.offsetHeight){e=r.body.scrollWidth;t=r.body.scrollHeight}else{e=r.body.offsetWidth;t=r.body.offsetHeight}if(self.innerHeight){n=self.innerWidth;i=self.innerHeight}else if(r.documentElement&&r.documentElement.clientHeight){n=r.documentElement.clientWidth;i=r.documentElement.clientHeight}else if(r.body){n=r.body.clientWidth;i=r.body.clientHeight}var s=Math.max(e,n);var o=Math.max(t,i);return[s,o,n,i]};n.prompt=function(t,n,i,s){var o;var u;if(i===undefined){i=""}var a=function(e){var t=e.charCode||e.keyCode;if(t===27){f(true)}};var f=function(t){e.removeEvent(r.body,"keydown",a);var n=u.value;if(t){n=null}else{n=n.replace(/^http:\/\/(https?|ftp):\/\//,"$1://");if(!/^(?:https?|ftp):\/\//.test(n))n="http://"+n}$(o).modal("hide");s(n);return false};var l=function(){o=r.createElement("div");o.className="modal fade";var s=r.createElement("div");s.className="modal-dialog";o.appendChild(s);var l=r.createElement("div");l.className="modal-content";s.appendChild(l);var c=r.createElement("div");c.className="modal-header";c.innerHTML='× ";l.appendChild(c);var h=r.createElement("div");h.className="modal-body";l.appendChild(h);var p=r.createElement("div");p.className="modal-footer";l.appendChild(p);var d=r.createElement("p");d.innerHTML=n;d.style.padding="5px";h.appendChild(d);var v=r.createElement("form"),m=v.style;v.onsubmit=function(){return f(false)};m.padding="0";m.margin="0";h.appendChild(v);u=r.createElement("input");u.type="text";u.value=i;u.className="form-control";m=u.style;m.display="block";m.width="80%";m.marginLeft=m.marginRight="auto";v.appendChild(u);var g=r.createElement("button");g.className="btn btn-primary";g.type="button";g.onclick=function(){return f(false)};g.innerHTML="OK";var y=r.createElement("button");y.className="btn btn-danger";y.type="button";y.onclick=function(){return f(true)};y.innerHTML="Cancel";p.appendChild(g);p.appendChild(y);e.addEvent(r.body,"keydown",a);r.body.appendChild(o)};setTimeout(function(){l();var e=i.length;if(u.selectionStart!==undefined){u.selectionStart=0;u.selectionEnd=e}else if(u.createTextRange){var t=u.createTextRange();t.collapse(false);t.moveStart("character",-e);t.moveEnd("character",e);t.select()}$(o).on("shown",function(){u.focus()});$(o).on("hidden",function(){o.parentNode.removeChild(o)});$(o).modal()},0)};var w=b.prototype;w.prefixes="(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";w.unwrap=function(e){var t=new i("([^\\n])\\n(?!(\\n|"+this.prefixes+"))","g");e.selection=e.selection.replace(t,"$1 $2")};w.wrap=function(e,t){this.unwrap(e);var n=new i("(.{1,"+t+"})( +|$\\n?)","gm"),r=this;e.selection=e.selection.replace(n,function(e,t){if((new i("^"+r.prefixes,"")).test(e)){return e}return t+"\n"});e.selection=e.selection.replace(/\s+$/,"")};w.doBold=function(e,t){return this.doBorI(e,t,2,"strong text")};w.doItalic=function(e,t){return this.doBorI(e,t,1,"emphasized text")};w.doBorI=function(e,t,n,r){e.trimWhitespace();e.selection=e.selection.replace(/\n{2,}/g,"\n");var s=/(\**$)/.exec(e.before)[0];var o=/(^\**)/.exec(e.after)[0];var u=Math.min(s.length,o.length);if(u>=n&&(u!=2||n!=1)){e.before=e.before.replace(i("[*]{"+n+"}$",""),"");e.after=e.after.replace(i("^[*]{"+n+"}",""),"")}else if(!e.selection&&o){e.after=e.after.replace(/^([*_]*)/,"");e.before=e.before.replace(/(\s?)$/,"");var a=i.$1;e.before=e.before+o+a}else{if(!e.selection&&!o){e.selection=r}var f=n<=1?"*":"**";e.before=e.before+f;e.after=f+e.after}return};w.stripLinkDefs=function(e,t){e=e.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,function(e,n,r,i,s){t[n]=e.replace(/\s*$/,"");if(i){t[n]=e.replace(/["(](.+?)[")]$/,"");return i+s}return""});return e};w.addLinkDef=function(e,t){var n=0;var r={};e.before=this.stripLinkDefs(e.before,r);e.selection=this.stripLinkDefs(e.selection,r);e.after=this.stripLinkDefs(e.after,r);var i="";var s=/(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;var o=function(e){n++;e=e.replace(/^[ ]{0,3}\[(\d+)\]:/," ["+n+"]:");i+="\n"+e};var u=function(e,t,i,a,f,l){i=i.replace(s,u);if(r[f]){o(r[f]);return t+i+a+n+l}return e};e.before=e.before.replace(s,u);if(t){o(t)}else{e.selection=e.selection.replace(s,u)}var a=n;e.after=e.after.replace(s,u);if(e.after){e.after=e.after.replace(/\n*$/,"")}if(!e.after){e.selection=e.selection.replace(/\n*$/,"")}e.after+="\n\n"+i;return a};w.doLinkOrImage=function(e,t,r){e.trimWhitespace();e.findTags(/\s*!?\[/,/\][ ]?(?:\n[ ]*)?(\[.*?\])?/);var i;if(e.endTag.length>1&&e.startTag.length>0){e.startTag=e.startTag.replace(/!?\[/,"");e.endTag="";this.addLinkDef(e,null)}else{e.selection=e.startTag+e.selection+e.endTag;e.startTag=e.endTag="";if(/\n\n/.test(e.selection)){this.addLinkDef(e,null);return}var s=this;var o=function(n){if(n!==null){e.selection=(" "+e.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g,"$1\\").substr(1);var i=" [999]: "+E(n);var o=s.addLinkDef(e,i);e.startTag=r?"![":"[";e.endTag="]["+o+"]";if(!e.selection){if(r){e.selection="enter image description here"}else{e.selection="enter link description here"}}}t()};if(r){if(!this.hooks.insertImageDialog(o))n.prompt("Insert Image",f,l,o)}else{n.prompt("Insert Link",a,c,o)}return true}};w.doAutoindent=function(e,t){var n=this,r=false;e.before=e.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/,"\n\n");e.before=e.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/,"\n\n");e.before=e.before.replace(/(\n|^)[ \t]+\n$/,"\n\n");if(!e.selection&&!/^[ \t]*(?:\n|$)/.test(e.after)){e.after=e.after.replace(/^[^\n]*/,function(t){e.selection=t;return""});r=true}if(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(e.before)){if(n.doList){n.doList(e)}}if(/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(e.before)){if(n.doBlockquote){n.doBlockquote(e)}}if(/(\n|^)(\t|[ ]{4,}).*\n$/.test(e.before)){if(n.doCode){n.doCode(e)}}if(r){e.after=e.selection+e.after;e.selection=""}};w.doBlockquote=function(e,t){e.selection=e.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,function(t,n,r,i){e.before+=n;e.after=i+e.after;return r});e.before=e.before.replace(/(>[ \t]*)$/,function(t,n){e.selection=n+e.selection;return""});e.selection=e.selection.replace(/^(\s|>)+$/,"");e.selection=e.selection||"Blockquote";var n="",r="",i;if(e.before){var s=e.before.replace(/\n$/,"").split("\n");var u=false;for(var a=0;a0;if(/^>/.test(i)){f=true;if(!u&&i.length>1)u=true}else if(/^[ \t]*$/.test(i)){f=true}else{f=u}if(f){n+=i+"\n"}else{r+=n+i;n="\n"}}if(!/(^|\n)>/.test(n)){r+=n;n=""}}e.startTag=n;e.before=r;if(e.after){e.after=e.after.replace(/^\n?/,"\n")}e.after=e.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,function(t){e.endTag=t;return""});var l=function(t){var n=t?"> ":"";if(e.startTag){e.startTag=e.startTag.replace(/\n((>|\s)*)\n$/,function(e,t){return"\n"+t.replace(/^[ ]{0,3}>?[ \t]*$/gm,n)+"\n"})}if(e.endTag){e.endTag=e.endTag.replace(/^\n((>|\s)*)\n/,function(e,t){return"\n"+t.replace(/^[ ]{0,3}>?[ \t]*$/gm,n)+"\n"})}};if(/^(?![ ]{0,3}>)/m.test(e.selection)){this.wrap(e,o.lineLength-2);e.selection=e.selection.replace(/^/gm,"> ");l(true);e.skipLines()}else{e.selection=e.selection.replace(/^[ ]{0,3}> ?/gm,"");this.unwrap(e);l(false);if(!/^(\n|^)[ ]{0,3}>/.test(e.selection)&&e.startTag){e.startTag=e.startTag.replace(/\n{0,2}$/,"\n\n")}if(!/(\n|^)[ ]{0,3}>.*$/.test(e.selection)&&e.endTag){e.endTag=e.endTag.replace(/^\n{0,2}/,"\n\n")}}e.selection=this.hooks.postBlockquoteCreation(e.selection);if(!/\n/.test(e.selection)){e.selection=e.selection.replace(/^(> *)/,function(t,n){e.startTag+=n;return""})}};w.doCode=function(e,t){var n=/\S[ ]*$/.test(e.before);var r=/^[ ]*\S/.test(e.after);if(!r&&!n||/\n/.test(e.selection)){e.before=e.before.replace(/[ ]{4}$/,function(t){e.selection=t+e.selection;return""});var i=1;var s=1;if(/(\n|^)(\t|[ ]{4,}).*\n$/.test(e.before)){i=0}if(/^\n(\t|[ ]{4,})/.test(e.after)){s=0}e.skipLines(i,s);if(!e.selection){e.startTag=" ";e.selection="enter code here"}else{if(/^[ ]{0,3}\S/m.test(e.selection)){if(/\n/.test(e.selection))e.selection=e.selection.replace(/^/gm," ");else e.before+=" "}else{e.selection=e.selection.replace(/^[ ]{4}/gm,"")}}}else{e.trimWhitespace();e.findTags(/`/,/`/);if(!e.startTag&&!e.endTag){e.startTag=e.endTag="`";if(!e.selection){e.selection="enter code here"}}else if(e.endTag&&!e.startTag){e.before+=e.endTag;e.endTag=""}else{e.startTag=e.endTag=""}}};w.doList=function(e,t,n){var r=/(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;var s=/^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;var u="-";var a=1;var f=function(){var e;if(n){e=" "+a+". ";a++}else{e=" "+u+" "}return e};var l=function(e){if(n===undefined){n=/^\s*\d/.test(e)}e=e.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,function(e){return f()});return e};e.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/,null);if(e.before&&!/\n$/.test(e.before)&&!/^\n/.test(e.startTag)){e.before+=e.startTag;e.startTag=""}if(e.startTag){var c=/\d+[.]/.test(e.startTag);e.startTag="";e.selection=e.selection.replace(/\n[ ]{4}/g,"\n");this.unwrap(e);e.skipLines();if(c){e.after=e.after.replace(s,l)}if(n==c){return}}var h=1;e.before=e.before.replace(r,function(e){if(/^\s*([*+-])/.test(e)){u=i.$1}h=/[^\n]\n\n[^\n]/.test(e)?1:0;return l(e)});if(!e.selection){e.selection="List item"}var p=f();var d=1;e.after=e.after.replace(s,function(e){d=/[^\n]\n\n[^\n]/.test(e)?1:0;return l(e)});e.trimWhitespace(true);e.skipLines(h,d,true);e.startTag=p;var v=p.replace(/./g," ");this.wrap(e,o.lineLength-v.length);e.selection=e.selection.replace(/\n/g,"\n"+v)};w.doHeading=function(e,t){e.selection=e.selection.replace(/\s+/g," ");e.selection=e.selection.replace(/(^\s+|\s+$)/g,"");if(!e.selection){e.startTag="## ";e.selection="Heading";e.endTag=" ##";return}var n=0;e.findTags(/#+[ ]*/,/[ ]*#+/);if(/#+/.test(e.startTag)){n=i.lastMatch.length}e.startTag=e.endTag="";e.findTags(null,/\s?(-+|=+)/);if(/=+/.test(e.endTag)){n=1}if(/-+/.test(e.endTag)){n=2}e.startTag=e.endTag="";e.skipLines(1,1);var r=n==0?2:n-1;if(r>0){var s=r>=2?"-":"=";var u=e.selection.length;if(u>o.lineLength){u=o.lineLength}e.endTag="\n";while(u--){e.endTag+=s}}};w.doHorizontalRule=function(e,t){e.startTag="----------\n";e.selection="";e.skipLines(2,1,true)}})();(function(){function n(e){return e.replace(/<[^>]*>?/gi,u)}function u(e){if(e.match(r)||e.match(i)||e.match(s)||e.match(o))return e;else return""}function a(e){if(e=="")return"";var t=/<\/?\w+[^>]*(\s|$|>)/g;var n=e.toLowerCase().match(t);var r=(n||[]).length;if(r==0)return e;var i,s;var o="



  • ";var u;var a=[];var f=[];var l=false;for(var c=0;c")>-1)continue;s=n[c];u=-1;if(!/^<\//.test(s)){for(var h=c+1;h"){u=h;break}}}if(u==-1)l=f[c]=true;else a[u]=true}if(!l)return e;var c=0;e=e.replace(t,function(e){var t=f[c]?"":e;c++;return t});return e}var e,t;if(typeof exports==="object"&&typeof require==="function"){e=exports;t=require("./Markdown.Converter").Converter}else{e=window.Markdown;t=e.Converter}e.getSanitizingConverter=function(){var e=new t;e.hooks.chain("postConversion",n);e.hooks.chain("postConversion",a);return e};var r=/^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;var i=/^(]+")?\s?>|<\/a>)$/i;var s=/^(]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;var o=/^(|<\/pre>)$/i})();(function(e){e.fn.pagedownBootstrap=function(t){var n=e.extend({sanitize:true,help:null,hooks:Array()},t);return this.each(function(){var t=null;if(n.sanitize){t=Markdown.getSanitizingConverter()}else{t=new Markdown.Converter}for(var r in n.hooks){var i=n.hooks[r];if(typeof i!=="object"||typeof i.event==="undefined"||typeof i.callback!=="function"){continue}t.hooks.chain(i.event,i.callback)}var s="wmd-input";var o=0;while(e("#"+s+"-"+o.toString()).length>0){o++}e(this).attr("id",s+"-"+o.toString());e(this).wrap('
    ');e(this).before('
    ');e(this).after('
    ');e(this).addClass("wmd-input");help=null;if(e.isFunction(n.help)){help={handler:n.help}}var u=new Markdown.Editor(t,"-"+o.toString(),help);u.run()})}})(jQuery) -------------------------------------------------------------------------------- /js/Markdown.Converter.js: -------------------------------------------------------------------------------- 1 | var Markdown; 2 | 3 | if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module 4 | Markdown = exports; 5 | else 6 | Markdown = {}; 7 | 8 | // The following text is included for historical reasons, but should 9 | // be taken with a pinch of salt; it's not all true anymore. 10 | 11 | // 12 | // Wherever possible, Showdown is a straight, line-by-line port 13 | // of the Perl version of Markdown. 14 | // 15 | // This is not a normal parser design; it's basically just a 16 | // series of string substitutions. It's hard to read and 17 | // maintain this way, but keeping Showdown close to the original 18 | // design makes it easier to port new features. 19 | // 20 | // More importantly, Showdown behaves like markdown.pl in most 21 | // edge cases. So web applications can do client-side preview 22 | // in Javascript, and then build identical HTML on the server. 23 | // 24 | // This port needs the new RegExp functionality of ECMA 262, 25 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 26 | // should do fine. Even with the new regular expression features, 27 | // We do a lot of work to emulate Perl's regex functionality. 28 | // The tricky changes in this file mostly have the "attacklab:" 29 | // label. Major or self-explanatory changes don't. 30 | // 31 | // Smart diff tools like Araxis Merge will be able to match up 32 | // this file with markdown.pl in a useful way. A little tweaking 33 | // helps: in a copy of markdown.pl, replace "#" with "//" and 34 | // replace "$text" with "text". Be sure to ignore whitespace 35 | // and line endings. 36 | // 37 | 38 | 39 | // 40 | // Usage: 41 | // 42 | // var text = "Markdown *rocks*."; 43 | // 44 | // var converter = new Markdown.Converter(); 45 | // var html = converter.makeHtml(text); 46 | // 47 | // alert(html); 48 | // 49 | // Note: move the sample code to the bottom of this 50 | // file before uncommenting it. 51 | // 52 | 53 | (function () { 54 | 55 | function identity(x) { return x; } 56 | function returnFalse(x) { return false; } 57 | 58 | function HookCollection() { } 59 | 60 | HookCollection.prototype = { 61 | 62 | chain: function (hookname, func) { 63 | var original = this[hookname]; 64 | if (!original) 65 | throw new Error("unknown hook " + hookname); 66 | 67 | if (original === identity) 68 | this[hookname] = func; 69 | else 70 | this[hookname] = function (x) { return func(original(x)); } 71 | }, 72 | set: function (hookname, func) { 73 | if (!this[hookname]) 74 | throw new Error("unknown hook " + hookname); 75 | this[hookname] = func; 76 | }, 77 | addNoop: function (hookname) { 78 | this[hookname] = identity; 79 | }, 80 | addFalse: function (hookname) { 81 | this[hookname] = returnFalse; 82 | } 83 | }; 84 | 85 | Markdown.HookCollection = HookCollection; 86 | 87 | // g_urls and g_titles allow arbitrary user-entered strings as keys. This 88 | // caused an exception (and hence stopped the rendering) when the user entered 89 | // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this 90 | // (since no builtin property starts with "s_"). See 91 | // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug 92 | // (granted, switching from Array() to Object() alone would have left only __proto__ 93 | // to be a problem) 94 | function SaveHash() { } 95 | SaveHash.prototype = { 96 | set: function (key, value) { 97 | this["s_" + key] = value; 98 | }, 99 | get: function (key) { 100 | return this["s_" + key]; 101 | } 102 | }; 103 | 104 | Markdown.Converter = function () { 105 | var pluginHooks = this.hooks = new HookCollection(); 106 | pluginHooks.addNoop("plainLinkText"); // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link 107 | pluginHooks.addNoop("preConversion"); // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked 108 | pluginHooks.addNoop("postConversion"); // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml 109 | 110 | // 111 | // Private state of the converter instance: 112 | // 113 | 114 | // Global hashes, used by various utility routines 115 | var g_urls; 116 | var g_titles; 117 | var g_html_blocks; 118 | 119 | // Used to track when we're inside an ordered or unordered list 120 | // (see _ProcessListItems() for details): 121 | var g_list_level; 122 | 123 | this.makeHtml = function (text) { 124 | 125 | // 126 | // Main function. The order in which other subs are called here is 127 | // essential. Link and image substitutions need to happen before 128 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 129 | // and tags get encoded. 130 | // 131 | 132 | // This will only happen if makeHtml on the same converter instance is called from a plugin hook. 133 | // Don't do that. 134 | if (g_urls) 135 | throw new Error("Recursive call to converter.makeHtml"); 136 | 137 | // Create the private state objects. 138 | g_urls = new SaveHash(); 139 | g_titles = new SaveHash(); 140 | g_html_blocks = []; 141 | g_list_level = 0; 142 | 143 | text = pluginHooks.preConversion(text); 144 | 145 | // attacklab: Replace ~ with ~T 146 | // This lets us use tilde as an escape char to avoid md5 hashes 147 | // The choice of character is arbitray; anything that isn't 148 | // magic in Markdown will work. 149 | text = text.replace(/~/g, "~T"); 150 | 151 | // attacklab: Replace $ with ~D 152 | // RegExp interprets $ as a special character 153 | // when it's in a replacement string 154 | text = text.replace(/\$/g, "~D"); 155 | 156 | // Standardize line endings 157 | text = text.replace(/\r\n/g, "\n"); // DOS to Unix 158 | text = text.replace(/\r/g, "\n"); // Mac to Unix 159 | 160 | // Make sure text begins and ends with a couple of newlines: 161 | text = "\n\n" + text + "\n\n"; 162 | 163 | // Convert all tabs to spaces. 164 | text = _Detab(text); 165 | 166 | // Strip any lines consisting only of spaces and tabs. 167 | // This makes subsequent regexen easier to write, because we can 168 | // match consecutive blank lines with /\n+/ instead of something 169 | // contorted like /[ \t]*\n+/ . 170 | text = text.replace(/^[ \t]+$/mg, ""); 171 | 172 | // Turn block-level HTML blocks into hash entries 173 | text = _HashHTMLBlocks(text); 174 | 175 | // Strip link definitions, store in hashes. 176 | text = _StripLinkDefinitions(text); 177 | 178 | text = _RunBlockGamut(text); 179 | 180 | text = _UnescapeSpecialChars(text); 181 | 182 | // attacklab: Restore dollar signs 183 | text = text.replace(/~D/g, "$$"); 184 | 185 | // attacklab: Restore tildes 186 | text = text.replace(/~T/g, "~"); 187 | 188 | text = pluginHooks.postConversion(text); 189 | 190 | g_html_blocks = g_titles = g_urls = null; 191 | 192 | return text; 193 | }; 194 | 195 | function _StripLinkDefinitions(text) { 196 | // 197 | // Strips link definitions from text, stores the URLs and titles in 198 | // hash references. 199 | // 200 | 201 | // Link defs are in the form: ^[id]: url "optional title" 202 | 203 | /* 204 | text = text.replace(/ 205 | ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 206 | [ \t]* 207 | \n? // maybe *one* newline 208 | [ \t]* 209 | ? // url = $2 210 | (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below 211 | [ \t]* 212 | \n? // maybe one newline 213 | [ \t]* 214 | ( // (potential) title = $3 215 | (\n*) // any lines skipped = $4 attacklab: lookbehind removed 216 | [ \t]+ 217 | ["(] 218 | (.+?) // title = $5 219 | [")] 220 | [ \t]* 221 | )? // title is optional 222 | (?:\n+|$) 223 | /gm, function(){...}); 224 | */ 225 | 226 | text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, 227 | function (wholeMatch, m1, m2, m3, m4, m5) { 228 | m1 = m1.toLowerCase(); 229 | g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive 230 | if (m4) { 231 | // Oops, found blank lines, so it's not a title. 232 | // Put back the parenthetical statement we stole. 233 | return m3; 234 | } else if (m5) { 235 | g_titles.set(m1, m5.replace(/"/g, """)); 236 | } 237 | 238 | // Completely remove the definition from the text 239 | return ""; 240 | } 241 | ); 242 | 243 | return text; 244 | } 245 | 246 | function _HashHTMLBlocks(text) { 247 | 248 | // Hashify HTML blocks: 249 | // We only want to do this for block-level HTML tags, such as headers, 250 | // lists, and tables. That's because we still want to wrap

    s around 251 | // "paragraphs" that are wrapped in non-block-level tags, such as anchors, 252 | // phrase emphasis, and spans. The list of tags we're looking for is 253 | // hard-coded: 254 | var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" 255 | var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" 256 | 257 | // First, look for nested blocks, e.g.: 258 | //

    259 | //
    260 | // tags for inner block must be indented. 261 | //
    262 | //
    263 | // 264 | // The outermost tags must start at the left margin for this to match, and 265 | // the inner nested divs must be indented. 266 | // We need to do this before the next, more liberal match, because the next 267 | // match will start at the first `
    ` and stop at the first `
    `. 268 | 269 | // attacklab: This regex can be expensive when it fails. 270 | 271 | /* 272 | text = text.replace(/ 273 | ( // save in $1 274 | ^ // start of line (with /m) 275 | <($block_tags_a) // start tag = $2 276 | \b // word break 277 | // attacklab: hack around khtml/pcre bug... 278 | [^\r]*?\n // any number of lines, minimally matching 279 | // the matching end tag 280 | [ \t]* // trailing spaces/tabs 281 | (?=\n+) // followed by a newline 282 | ) // attacklab: there are sentinel newlines at end of document 283 | /gm,function(){...}}; 284 | */ 285 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashElement); 286 | 287 | // 288 | // Now match more liberally, simply from `\n` to `\n` 289 | // 290 | 291 | /* 292 | text = text.replace(/ 293 | ( // save in $1 294 | ^ // start of line (with /m) 295 | <($block_tags_b) // start tag = $2 296 | \b // word break 297 | // attacklab: hack around khtml/pcre bug... 298 | [^\r]*? // any number of lines, minimally matching 299 | .* // the matching end tag 300 | [ \t]* // trailing spaces/tabs 301 | (?=\n+) // followed by a newline 302 | ) // attacklab: there are sentinel newlines at end of document 303 | /gm,function(){...}}; 304 | */ 305 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashElement); 306 | 307 | // Special case just for
    . It was easier to make a special case than 308 | // to make the other regex more complicated. 309 | 310 | /* 311 | text = text.replace(/ 312 | \n // Starting after a blank line 313 | [ ]{0,3} 314 | ( // save in $1 315 | (<(hr) // start tag = $2 316 | \b // word break 317 | ([^<>])*? 318 | \/?>) // the matching end tag 319 | [ \t]* 320 | (?=\n{2,}) // followed by a blank line 321 | ) 322 | /g,hashElement); 323 | */ 324 | text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement); 325 | 326 | // Special case for standalone HTML comments: 327 | 328 | /* 329 | text = text.replace(/ 330 | \n\n // Starting after a blank line 331 | [ ]{0,3} // attacklab: g_tab_width - 1 332 | ( // save in $1 333 | -]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackoverflow.com/q/95256 335 | > 336 | [ \t]* 337 | (?=\n{2,}) // followed by a blank line 338 | ) 339 | /g,hashElement); 340 | */ 341 | text = text.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashElement); 342 | 343 | // PHP and ASP-style processor instructions ( and <%...%>) 344 | 345 | /* 346 | text = text.replace(/ 347 | (?: 348 | \n\n // Starting after a blank line 349 | ) 350 | ( // save in $1 351 | [ ]{0,3} // attacklab: g_tab_width - 1 352 | (?: 353 | <([?%]) // $2 354 | [^\r]*? 355 | \2> 356 | ) 357 | [ \t]* 358 | (?=\n{2,}) // followed by a blank line 359 | ) 360 | /g,hashElement); 361 | */ 362 | text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement); 363 | 364 | return text; 365 | } 366 | 367 | function hashElement(wholeMatch, m1) { 368 | var blockText = m1; 369 | 370 | // Undo double lines 371 | blockText = blockText.replace(/^\n+/, ""); 372 | 373 | // strip trailing blank lines 374 | blockText = blockText.replace(/\n+$/g, ""); 375 | 376 | // Replace the element text with a marker ("~KxK" where x is its key) 377 | blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n"; 378 | 379 | return blockText; 380 | } 381 | 382 | function _RunBlockGamut(text, doNotUnhash) { 383 | // 384 | // These are all the transformations that form block-level 385 | // tags like paragraphs, headers, and list items. 386 | // 387 | text = _DoHeaders(text); 388 | 389 | // Do Horizontal Rules: 390 | var replacement = "
    \n"; 391 | text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement); 392 | text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement); 393 | text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement); 394 | 395 | text = _DoLists(text); 396 | text = _DoCodeBlocks(text); 397 | text = _DoBlockQuotes(text); 398 | 399 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 400 | // was to escape raw HTML in the original Markdown source. This time, 401 | // we're escaping the markup we've just created, so that we don't wrap 402 | //

    tags around block-level tags. 403 | text = _HashHTMLBlocks(text); 404 | text = _FormParagraphs(text, doNotUnhash); 405 | 406 | return text; 407 | } 408 | 409 | function _RunSpanGamut(text) { 410 | // 411 | // These are all the transformations that occur *within* block-level 412 | // tags like paragraphs, headers, and list items. 413 | // 414 | 415 | text = _DoCodeSpans(text); 416 | text = _EscapeSpecialCharsWithinTagAttributes(text); 417 | text = _EncodeBackslashEscapes(text); 418 | 419 | // Process anchor and image tags. Images must come first, 420 | // because ![foo][f] looks like an anchor. 421 | text = _DoImages(text); 422 | text = _DoAnchors(text); 423 | 424 | // Make links out of things like `` 425 | // Must come after _DoAnchors(), because you can use < and > 426 | // delimiters in inline links like [this](). 427 | text = _DoAutoLinks(text); 428 | 429 | text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now 430 | 431 | text = _EncodeAmpsAndAngles(text); 432 | text = _DoItalicsAndBold(text); 433 | 434 | // Do hard breaks: 435 | text = text.replace(/ +\n/g, "
    \n"); 436 | 437 | return text; 438 | } 439 | 440 | function _EscapeSpecialCharsWithinTagAttributes(text) { 441 | // 442 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they 443 | // don't conflict with their use in Markdown for code, italics and strong. 444 | // 445 | 446 | // Build a regex to find HTML tags and comments. See Friedl's 447 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. 448 | 449 | // SE: changed the comment part of the regex 450 | 451 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi; 452 | 453 | text = text.replace(regex, function (wholeMatch) { 454 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`"); 455 | tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987 456 | return tag; 457 | }); 458 | 459 | return text; 460 | } 461 | 462 | function _DoAnchors(text) { 463 | // 464 | // Turn Markdown link shortcuts into XHTML
    tags. 465 | // 466 | // 467 | // First, handle reference-style links: [link text] [id] 468 | // 469 | 470 | /* 471 | text = text.replace(/ 472 | ( // wrap whole match in $1 473 | \[ 474 | ( 475 | (?: 476 | \[[^\]]*\] // allow brackets nested one level 477 | | 478 | [^\[] // or anything else 479 | )* 480 | ) 481 | \] 482 | 483 | [ ]? // one optional space 484 | (?:\n[ ]*)? // one optional newline followed by spaces 485 | 486 | \[ 487 | (.*?) // id = $3 488 | \] 489 | ) 490 | ()()()() // pad remaining backreferences 491 | /g, writeAnchorTag); 492 | */ 493 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag); 494 | 495 | // 496 | // Next, inline-style links: [link text](url "optional title") 497 | // 498 | 499 | /* 500 | text = text.replace(/ 501 | ( // wrap whole match in $1 502 | \[ 503 | ( 504 | (?: 505 | \[[^\]]*\] // allow brackets nested one level 506 | | 507 | [^\[\]] // or anything else 508 | )* 509 | ) 510 | \] 511 | \( // literal paren 512 | [ \t]* 513 | () // no id, so leave $3 empty 514 | ? 521 | [ \t]* 522 | ( // $5 523 | (['"]) // quote char = $6 524 | (.*?) // Title = $7 525 | \6 // matching quote 526 | [ \t]* // ignore any spaces/tabs between closing quote and ) 527 | )? // title is optional 528 | \) 529 | ) 530 | /g, writeAnchorTag); 531 | */ 532 | 533 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag); 534 | 535 | // 536 | // Last, handle reference-style shortcuts: [link text] 537 | // These must come last in case you've also got [link test][1] 538 | // or [link test](/foo) 539 | // 540 | 541 | /* 542 | text = text.replace(/ 543 | ( // wrap whole match in $1 544 | \[ 545 | ([^\[\]]+) // link text = $2; can't contain '[' or ']' 546 | \] 547 | ) 548 | ()()()()() // pad rest of backreferences 549 | /g, writeAnchorTag); 550 | */ 551 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); 552 | 553 | return text; 554 | } 555 | 556 | function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) { 557 | if (m7 == undefined) m7 = ""; 558 | var whole_match = m1; 559 | var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs 560 | var link_id = m3.toLowerCase(); 561 | var url = m4; 562 | var title = m7; 563 | 564 | if (url == "") { 565 | if (link_id == "") { 566 | // lower-case and turn embedded newlines into spaces 567 | link_id = link_text.toLowerCase().replace(/ ?\n/g, " "); 568 | } 569 | url = "#" + link_id; 570 | 571 | if (g_urls.get(link_id) != undefined) { 572 | url = g_urls.get(link_id); 573 | if (g_titles.get(link_id) != undefined) { 574 | title = g_titles.get(link_id); 575 | } 576 | } 577 | else { 578 | if (whole_match.search(/\(\s*\)$/m) > -1) { 579 | // Special case for explicit empty url 580 | url = ""; 581 | } else { 582 | return whole_match; 583 | } 584 | } 585 | } 586 | url = encodeProblemUrlChars(url); 587 | url = escapeCharacters(url, "*_"); 588 | var result = ""; 597 | 598 | return result; 599 | } 600 | 601 | function _DoImages(text) { 602 | // 603 | // Turn Markdown image shortcuts into tags. 604 | // 605 | 606 | // 607 | // First, handle reference-style labeled images: ![alt text][id] 608 | // 609 | 610 | /* 611 | text = text.replace(/ 612 | ( // wrap whole match in $1 613 | !\[ 614 | (.*?) // alt text = $2 615 | \] 616 | 617 | [ ]? // one optional space 618 | (?:\n[ ]*)? // one optional newline followed by spaces 619 | 620 | \[ 621 | (.*?) // id = $3 622 | \] 623 | ) 624 | ()()()() // pad rest of backreferences 625 | /g, writeImageTag); 626 | */ 627 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag); 628 | 629 | // 630 | // Next, handle inline images: ![alt text](url "optional title") 631 | // Don't forget: encode * and _ 632 | 633 | /* 634 | text = text.replace(/ 635 | ( // wrap whole match in $1 636 | !\[ 637 | (.*?) // alt text = $2 638 | \] 639 | \s? // One optional whitespace character 640 | \( // literal paren 641 | [ \t]* 642 | () // no id, so leave $3 empty 643 | ? // src url = $4 644 | [ \t]* 645 | ( // $5 646 | (['"]) // quote char = $6 647 | (.*?) // title = $7 648 | \6 // matching quote 649 | [ \t]* 650 | )? // title is optional 651 | \) 652 | ) 653 | /g, writeImageTag); 654 | */ 655 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag); 656 | 657 | return text; 658 | } 659 | 660 | function attributeEncode(text) { 661 | // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title) 662 | // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it) 663 | return text.replace(/>/g, ">").replace(/" + _RunSpanGamut(m1) + "\n\n"; } 722 | ); 723 | 724 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, 725 | function (matchFound, m1) { return "

    " + _RunSpanGamut(m1) + "

    \n\n"; } 726 | ); 727 | 728 | // atx-style headers: 729 | // # Header 1 730 | // ## Header 2 731 | // ## Header 2 with closing hashes ## 732 | // ... 733 | // ###### Header 6 734 | // 735 | 736 | /* 737 | text = text.replace(/ 738 | ^(\#{1,6}) // $1 = string of #'s 739 | [ \t]* 740 | (.+?) // $2 = Header text 741 | [ \t]* 742 | \#* // optional closing #'s (not counted) 743 | \n+ 744 | /gm, function() {...}); 745 | */ 746 | 747 | text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, 748 | function (wholeMatch, m1, m2) { 749 | var h_level = m1.length; 750 | return "" + _RunSpanGamut(m2) + "\n\n"; 751 | } 752 | ); 753 | 754 | return text; 755 | } 756 | 757 | function _DoLists(text) { 758 | // 759 | // Form HTML ordered (numbered) and unordered (bulleted) lists. 760 | // 761 | 762 | // attacklab: add sentinel to hack around khtml/safari bug: 763 | // http://bugs.webkit.org/show_bug.cgi?id=11231 764 | text += "~0"; 765 | 766 | // Re-usable pattern to match any entirel ul or ol list: 767 | 768 | /* 769 | var whole_list = / 770 | ( // $1 = whole list 771 | ( // $2 772 | [ ]{0,3} // attacklab: g_tab_width - 1 773 | ([*+-]|\d+[.]) // $3 = first list item marker 774 | [ \t]+ 775 | ) 776 | [^\r]+? 777 | ( // $4 778 | ~0 // sentinel for workaround; should be $ 779 | | 780 | \n{2,} 781 | (?=\S) 782 | (?! // Negative lookahead for another list item marker 783 | [ \t]* 784 | (?:[*+-]|\d+[.])[ \t]+ 785 | ) 786 | ) 787 | ) 788 | /g 789 | */ 790 | var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 791 | 792 | if (g_list_level) { 793 | text = text.replace(whole_list, function (wholeMatch, m1, m2) { 794 | var list = m1; 795 | var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; 796 | 797 | var result = _ProcessListItems(list, list_type); 798 | 799 | // Trim any trailing whitespace, to put the closing `` 800 | // up on the preceding line, to get it past the current stupid 801 | // HTML block parser. This is a hack to work around the terrible 802 | // hack that is the HTML block parser. 803 | result = result.replace(/\s+$/, ""); 804 | result = "<" + list_type + ">" + result + "\n"; 805 | return result; 806 | }); 807 | } else { 808 | whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; 809 | text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { 810 | var runup = m1; 811 | var list = m2; 812 | 813 | var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; 814 | var result = _ProcessListItems(list, list_type); 815 | result = runup + "<" + list_type + ">\n" + result + "\n"; 816 | return result; 817 | }); 818 | } 819 | 820 | // attacklab: strip sentinel 821 | text = text.replace(/~0/, ""); 822 | 823 | return text; 824 | } 825 | 826 | var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" }; 827 | 828 | function _ProcessListItems(list_str, list_type) { 829 | // 830 | // Process the contents of a single ordered or unordered list, splitting it 831 | // into individual list items. 832 | // 833 | // list_type is either "ul" or "ol". 834 | 835 | // The $g_list_level global keeps track of when we're inside a list. 836 | // Each time we enter a list, we increment it; when we leave a list, 837 | // we decrement. If it's zero, we're not in a list anymore. 838 | // 839 | // We do this because when we're not inside a list, we want to treat 840 | // something like this: 841 | // 842 | // I recommend upgrading to version 843 | // 8. Oops, now this line is treated 844 | // as a sub-list. 845 | // 846 | // As a single paragraph, despite the fact that the second line starts 847 | // with a digit-period-space sequence. 848 | // 849 | // Whereas when we're inside a list (or sub-list), that line will be 850 | // treated as the start of a sub-list. What a kludge, huh? This is 851 | // an aspect of Markdown's syntax that's hard to parse perfectly 852 | // without resorting to mind-reading. Perhaps the solution is to 853 | // change the syntax rules such that sub-lists must start with a 854 | // starting cardinal number; e.g. "1." or "a.". 855 | 856 | g_list_level++; 857 | 858 | // trim trailing blank lines: 859 | list_str = list_str.replace(/\n{2,}$/, "\n"); 860 | 861 | // attacklab: add sentinel to emulate \z 862 | list_str += "~0"; 863 | 864 | // In the original attacklab showdown, list_type was not given to this function, and anything 865 | // that matched /[*+-]|\d+[.]/ would just create the next
  • , causing this mismatch: 866 | // 867 | // Markdown rendered by WMD rendered by MarkdownSharp 868 | // ------------------------------------------------------------------ 869 | // 1. first 1. first 1. first 870 | // 2. second 2. second 2. second 871 | // - third 3. third * third 872 | // 873 | // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx, 874 | // with {MARKER} being one of \d+[.] or [*+-], depending on list_type: 875 | 876 | /* 877 | list_str = list_str.replace(/ 878 | (^[ \t]*) // leading whitespace = $1 879 | ({MARKER}) [ \t]+ // list marker = $2 880 | ([^\r]+? // list item text = $3 881 | (\n+) 882 | ) 883 | (?= 884 | (~0 | \2 ({MARKER}) [ \t]+) 885 | ) 886 | /gm, function(){...}); 887 | */ 888 | 889 | var marker = _listItemMarkers[list_type]; 890 | var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm"); 891 | var last_item_had_a_double_newline = false; 892 | list_str = list_str.replace(re, 893 | function (wholeMatch, m1, m2, m3) { 894 | var item = m3; 895 | var leading_space = m1; 896 | var ends_with_double_newline = /\n\n$/.test(item); 897 | var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1; 898 | 899 | if (contains_double_newline || last_item_had_a_double_newline) { 900 | item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true); 901 | } 902 | else { 903 | // Recursion for sub-lists: 904 | item = _DoLists(_Outdent(item)); 905 | item = item.replace(/\n$/, ""); // chomp(item) 906 | item = _RunSpanGamut(item); 907 | } 908 | last_item_had_a_double_newline = ends_with_double_newline; 909 | return "
  • " + item + "
  • \n"; 910 | } 911 | ); 912 | 913 | // attacklab: strip sentinel 914 | list_str = list_str.replace(/~0/g, ""); 915 | 916 | g_list_level--; 917 | return list_str; 918 | } 919 | 920 | function _DoCodeBlocks(text) { 921 | // 922 | // Process Markdown `
    ` blocks.
     923 | 			//  
     924 | 
     925 | 			/*
     926 | 			text = text.replace(/
     927 | 				(?:\n\n|^)
     928 | 				(                               // $1 = the code block -- one or more lines, starting with a space/tab
     929 | 					(?:
     930 | 						(?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
     931 | 						.*\n+
     932 | 					)+
     933 | 				)
     934 | 				(\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
     935 | 			/g ,function(){...});
     936 | 			*/
     937 | 
     938 | 			// attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
     939 | 			text += "~0";
     940 | 
     941 | 			text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
     942 | 				function (wholeMatch, m1, m2) {
     943 | 					var codeblock = m1;
     944 | 					var nextChar = m2;
     945 | 
     946 | 					codeblock = _EncodeCode(_Outdent(codeblock));
     947 | 					codeblock = _Detab(codeblock);
     948 | 					codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
     949 | 					codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
     950 | 
     951 | 					codeblock = '
    ' + codeblock + '\n
    '; 952 | 953 | return "\n\n" + codeblock + "\n\n" + nextChar; 954 | } 955 | ); 956 | 957 | // attacklab: strip sentinel 958 | text = text.replace(/~0/, ""); 959 | 960 | return text; 961 | } 962 | 963 | function hashBlock(text) { 964 | text = text.replace(/(^\n+|\n+$)/g, ""); 965 | return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n"; 966 | } 967 | 968 | function _DoCodeSpans(text) { 969 | // 970 | // * Backtick quotes are used for spans. 971 | // 972 | // * You can use multiple backticks as the delimiters if you want to 973 | // include literal backticks in the code span. So, this input: 974 | // 975 | // Just type ``foo `bar` baz`` at the prompt. 976 | // 977 | // Will translate to: 978 | // 979 | //

    Just type foo `bar` baz at the prompt.

    980 | // 981 | // There's no arbitrary limit to the number of backticks you 982 | // can use as delimters. If you need three consecutive backticks 983 | // in your code, use four for delimiters, etc. 984 | // 985 | // * You can use spaces to get literal backticks at the edges: 986 | // 987 | // ... type `` `bar` `` ... 988 | // 989 | // Turns to: 990 | // 991 | // ... type `bar` ... 992 | // 993 | 994 | /* 995 | text = text.replace(/ 996 | (^|[^\\]) // Character before opening ` can't be a backslash 997 | (`+) // $2 = Opening run of ` 998 | ( // $3 = The code block 999 | [^\r]*? 1000 | [^`] // attacklab: work around lack of lookbehind 1001 | ) 1002 | \2 // Matching closer 1003 | (?!`) 1004 | /gm, function(){...}); 1005 | */ 1006 | 1007 | text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, 1008 | function (wholeMatch, m1, m2, m3, m4) { 1009 | var c = m3; 1010 | c = c.replace(/^([ \t]*)/g, ""); // leading whitespace 1011 | c = c.replace(/[ \t]*$/g, ""); // trailing whitespace 1012 | c = _EncodeCode(c); 1013 | c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs. 1014 | return m1 + "" + c + ""; 1015 | } 1016 | ); 1017 | 1018 | return text; 1019 | } 1020 | 1021 | function _EncodeCode(text) { 1022 | // 1023 | // Encode/escape certain characters inside Markdown code runs. 1024 | // The point is that in code, these characters are literals, 1025 | // and lose their special Markdown meanings. 1026 | // 1027 | // Encode all ampersands; HTML entities are not 1028 | // entities within a Markdown code span. 1029 | text = text.replace(/&/g, "&"); 1030 | 1031 | // Do the angle bracket song and dance: 1032 | text = text.replace(//g, ">"); 1034 | 1035 | // Now, escape characters that are magic in Markdown: 1036 | text = escapeCharacters(text, "\*_{}[]\\", false); 1037 | 1038 | // jj the line above breaks this: 1039 | //--- 1040 | 1041 | //* Item 1042 | 1043 | // 1. Subitem 1044 | 1045 | // special char: * 1046 | //--- 1047 | 1048 | return text; 1049 | } 1050 | 1051 | function _DoItalicsAndBold(text) { 1052 | 1053 | // must go first: 1054 | text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g, 1055 | "$1$3$4"); 1056 | 1057 | text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g, 1058 | "$1$3$4"); 1059 | 1060 | return text; 1061 | } 1062 | 1063 | function _DoBlockQuotes(text) { 1064 | 1065 | /* 1066 | text = text.replace(/ 1067 | ( // Wrap whole match in $1 1068 | ( 1069 | ^[ \t]*>[ \t]? // '>' at the start of a line 1070 | .+\n // rest of the first line 1071 | (.+\n)* // subsequent consecutive lines 1072 | \n* // blanks 1073 | )+ 1074 | ) 1075 | /gm, function(){...}); 1076 | */ 1077 | 1078 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, 1079 | function (wholeMatch, m1) { 1080 | var bq = m1; 1081 | 1082 | // attacklab: hack around Konqueror 3.5.4 bug: 1083 | // "----------bug".replace(/^-/g,"") == "bug" 1084 | 1085 | bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting 1086 | 1087 | // attacklab: clean up hack 1088 | bq = bq.replace(/~0/g, ""); 1089 | 1090 | bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines 1091 | bq = _RunBlockGamut(bq); // recurse 1092 | 1093 | bq = bq.replace(/(^|\n)/g, "$1 "); 1094 | // These leading spaces screw with
     content, so we need to fix that:
    1095 | 					bq = bq.replace(
    1096 | 							/(\s*
    [^\r]+?<\/pre>)/gm,
    1097 | 						function (wholeMatch, m1) {
    1098 | 							var pre = m1;
    1099 | 							// attacklab: hack around Konqueror 3.5.4 bug:
    1100 | 							pre = pre.replace(/^  /mg, "~0");
    1101 | 							pre = pre.replace(/~0/g, "");
    1102 | 							return pre;
    1103 | 						});
    1104 | 
    1105 | 					return hashBlock("
    \n" + bq + "\n
    "); 1106 | } 1107 | ); 1108 | return text; 1109 | } 1110 | 1111 | function _FormParagraphs(text, doNotUnhash) { 1112 | // 1113 | // Params: 1114 | // $text - string to process with html

    tags 1115 | // 1116 | 1117 | // Strip leading and trailing lines: 1118 | text = text.replace(/^\n+/g, ""); 1119 | text = text.replace(/\n+$/g, ""); 1120 | 1121 | var grafs = text.split(/\n{2,}/g); 1122 | var grafsOut = []; 1123 | 1124 | var markerRe = /~K(\d+)K/; 1125 | 1126 | // 1127 | // Wrap

    tags. 1128 | // 1129 | var end = grafs.length; 1130 | for (var i = 0; i < end; i++) { 1131 | var str = grafs[i]; 1132 | 1133 | // if this is an HTML marker, copy it 1134 | if (markerRe.test(str)) { 1135 | grafsOut.push(str); 1136 | } 1137 | else if (/\S/.test(str)) { 1138 | str = _RunSpanGamut(str); 1139 | str = str.replace(/^([ \t]*)/g, "

    "); 1140 | str += "

    " 1141 | grafsOut.push(str); 1142 | } 1143 | 1144 | } 1145 | // 1146 | // Unhashify HTML blocks 1147 | // 1148 | if (!doNotUnhash) { 1149 | end = grafsOut.length; 1150 | for (var i = 0; i < end; i++) { 1151 | var foundAny = true; 1152 | while (foundAny) { // we may need several runs, since the data may be nested 1153 | foundAny = false; 1154 | grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { 1155 | foundAny = true; 1156 | return g_html_blocks[id]; 1157 | }); 1158 | } 1159 | } 1160 | } 1161 | return grafsOut.join("\n\n"); 1162 | } 1163 | 1164 | function _EncodeAmpsAndAngles(text) { 1165 | // Smart processing for ampersands and angle brackets that need to be encoded. 1166 | 1167 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1168 | // http://bumppo.net/projects/amputator/ 1169 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); 1170 | 1171 | // Encode naked <'s 1172 | text = text.replace(/<(?![a-z\/?\$!])/gi, "<"); 1173 | 1174 | return text; 1175 | } 1176 | 1177 | function _EncodeBackslashEscapes(text) { 1178 | // 1179 | // Parameter: String. 1180 | // Returns: The string, with after processing the following backslash 1181 | // escape sequences. 1182 | // 1183 | 1184 | // attacklab: The polite way to do this is with the new 1185 | // escapeCharacters() function: 1186 | // 1187 | // text = escapeCharacters(text,"\\",true); 1188 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1189 | // 1190 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1191 | // as an optimization for Firefox. This function gets called a LOT. 1192 | 1193 | text = text.replace(/\\(\\)/g, escapeCharacters_callback); 1194 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); 1195 | return text; 1196 | } 1197 | 1198 | function _DoAutoLinks(text) { 1199 | 1200 | // note that at this point, all other URL in the text are already hyperlinked as
    1201 | // *except* for the case 1202 | 1203 | // automatically add < and > around unadorned raw hyperlinks 1204 | // must be preceded by space/BOF and followed by non-word/EOF character 1205 | text = text.replace(/(^|\s)(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\]])($|\W)/gi, "$1<$2$3>$4"); 1206 | 1207 | // autolink anything like 1208 | 1209 | var replacer = function (wholematch, m1) { return "" + pluginHooks.plainLinkText(m1) + ""; } 1210 | text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); 1211 | 1212 | // Email addresses: 1213 | /* 1214 | text = text.replace(/ 1215 | < 1216 | (?:mailto:)? 1217 | ( 1218 | [-.\w]+ 1219 | \@ 1220 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1221 | ) 1222 | > 1223 | /gi, _DoAutoLinks_callback()); 1224 | */ 1225 | 1226 | var email_replacer = function(wholematch, m1) { 1227 | var mailto = 'mailto:' 1228 | var link 1229 | var email 1230 | if (m1.substring(0, mailto.length) != mailto){ 1231 | link = mailto + m1; 1232 | email = m1; 1233 | } else { 1234 | link = m1; 1235 | email = m1.substring(mailto.length, m1.length); 1236 | } 1237 | return "" + pluginHooks.plainLinkText(email) + ""; 1238 | } 1239 | text = text.replace(/<((?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+))>/gi, email_replacer); 1240 | 1241 | return text; 1242 | } 1243 | 1244 | function _UnescapeSpecialChars(text) { 1245 | // 1246 | // Swap back in all the special characters we've hidden. 1247 | // 1248 | text = text.replace(/~E(\d+)E/g, 1249 | function (wholeMatch, m1) { 1250 | var charCodeToReplace = parseInt(m1); 1251 | return String.fromCharCode(charCodeToReplace); 1252 | } 1253 | ); 1254 | return text; 1255 | } 1256 | 1257 | function _Outdent(text) { 1258 | // 1259 | // Remove one level of line-leading tabs or spaces 1260 | // 1261 | 1262 | // attacklab: hack around Konqueror 3.5.4 bug: 1263 | // "----------bug".replace(/^-/g,"") == "bug" 1264 | 1265 | text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width 1266 | 1267 | // attacklab: clean up hack 1268 | text = text.replace(/~0/g, "") 1269 | 1270 | return text; 1271 | } 1272 | 1273 | function _Detab(text) { 1274 | if (!/\t/.test(text)) 1275 | return text; 1276 | 1277 | var spaces = [" ", " ", " ", " "], 1278 | skew = 0, 1279 | v; 1280 | 1281 | return text.replace(/[\n\t]/g, function (match, offset) { 1282 | if (match === "\n") { 1283 | skew = offset + 1; 1284 | return match; 1285 | } 1286 | v = (offset - skew) % 4; 1287 | skew = offset + 1; 1288 | return spaces[v]; 1289 | }); 1290 | } 1291 | 1292 | // 1293 | // attacklab: Utility functions 1294 | // 1295 | 1296 | var _problemUrlChars = /(?:["'*()[\]:]|~D)/g; 1297 | 1298 | // hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems 1299 | function encodeProblemUrlChars(url) { 1300 | if (!url) 1301 | return ""; 1302 | 1303 | var len = url.length; 1304 | 1305 | return url.replace(_problemUrlChars, function (match, offset) { 1306 | if (match == "~D") // escape for dollar 1307 | return "%24"; 1308 | if (match == ":") { 1309 | if (offset == len - 1 || /[0-9\/]/.test(url.charAt(offset + 1))) 1310 | return ":"; 1311 | if (url.substring(0, 'mailto:'.length) === 'mailto:') 1312 | return ":"; 1313 | if (url.substring(0, 'magnet:'.length) === 'magnet:') 1314 | return ":"; 1315 | } 1316 | return "%" + match.charCodeAt(0).toString(16); 1317 | }); 1318 | } 1319 | 1320 | 1321 | function escapeCharacters(text, charsToEscape, afterBackslash) { 1322 | // First we have to escape the escape characters so that 1323 | // we can build a character class out of them 1324 | var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; 1325 | 1326 | if (afterBackslash) { 1327 | regexString = "\\\\" + regexString; 1328 | } 1329 | 1330 | var regex = new RegExp(regexString, "g"); 1331 | text = text.replace(regex, escapeCharacters_callback); 1332 | 1333 | return text; 1334 | } 1335 | 1336 | 1337 | function escapeCharacters_callback(wholeMatch, m1) { 1338 | var charCodeToEscape = m1.charCodeAt(0); 1339 | return "~E" + charCodeToEscape + "E"; 1340 | } 1341 | 1342 | }; // end of the Markdown.Converter constructor 1343 | 1344 | })(); 1345 | -------------------------------------------------------------------------------- /js/Markdown.Editor.js: -------------------------------------------------------------------------------- 1 | // needs Markdown.Converter.js at the moment 2 | 3 | (function () { 4 | 5 | var util = {}, 6 | position = {}, 7 | ui = {}, 8 | doc = window.document, 9 | re = window.RegExp, 10 | nav = window.navigator, 11 | SETTINGS = { lineLength: 72 }, 12 | 13 | // Used to work around some browser bugs where we can't use feature testing. 14 | uaSniffed = { 15 | isIE: /msie/.test(nav.userAgent.toLowerCase()), 16 | isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()), 17 | isOpera: /opera/.test(nav.userAgent.toLowerCase()) 18 | }; 19 | 20 | 21 | // ------------------------------------------------------------------- 22 | // YOUR CHANGES GO HERE 23 | // 24 | // I've tried to localize the things you are likely to change to 25 | // this area. 26 | // ------------------------------------------------------------------- 27 | 28 | // The text that appears on the upper part of the dialog box when 29 | // entering links. 30 | var linkDialogText = "http://example.com/ \"optional title\""; 31 | var imageDialogText = "http://example.com/images/diagram.jpg \"optional title\""; 32 | 33 | // The default text that appears in the dialog input box when entering 34 | // links. 35 | var imageDefaultText = "http://"; 36 | var linkDefaultText = "http://"; 37 | 38 | var defaultHelpHoverTitle = "Markdown Editing Help"; 39 | 40 | // ------------------------------------------------------------------- 41 | // END OF YOUR CHANGES 42 | // ------------------------------------------------------------------- 43 | 44 | // help, if given, should have a property "handler", the click handler for the help button, 45 | // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help"). 46 | // If help isn't given, not help button is created. 47 | // 48 | // The constructed editor object has the methods: 49 | // - getConverter() returns the markdown converter object that was passed to the constructor 50 | // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. 51 | // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. 52 | Markdown.Editor = function (markdownConverter, idPostfix, help) { 53 | 54 | idPostfix = idPostfix || ""; 55 | 56 | var hooks = this.hooks = new Markdown.HookCollection(); 57 | hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed 58 | hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text 59 | hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates 60 | * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen 61 | * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. 62 | */ 63 | 64 | this.getConverter = function () { return markdownConverter; } 65 | 66 | var that = this, 67 | panels; 68 | 69 | this.run = function () { 70 | if (panels) 71 | return; // already initialized 72 | 73 | panels = new PanelCollection(idPostfix); 74 | var commandManager = new CommandManager(hooks); 75 | var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); }); 76 | var undoManager, uiManager; 77 | 78 | if (!/\?noundo/.test(doc.location.href)) { 79 | undoManager = new UndoManager(function () { 80 | previewManager.refresh(); 81 | if (uiManager) // not available on the first call 82 | uiManager.setUndoRedoButtonStates(); 83 | }, panels); 84 | this.textOperation = function (f) { 85 | undoManager.setCommandMode(); 86 | f(); 87 | that.refreshPreview(); 88 | } 89 | } 90 | 91 | uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help); 92 | uiManager.setUndoRedoButtonStates(); 93 | 94 | var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); }; 95 | 96 | forceRefresh(); 97 | }; 98 | 99 | } 100 | 101 | // before: contains all the text in the input box BEFORE the selection. 102 | // after: contains all the text in the input box AFTER the selection. 103 | function Chunks() { } 104 | 105 | // startRegex: a regular expression to find the start tag 106 | // endRegex: a regular expresssion to find the end tag 107 | Chunks.prototype.findTags = function (startRegex, endRegex) { 108 | 109 | var chunkObj = this; 110 | var regex; 111 | 112 | if (startRegex) { 113 | 114 | regex = util.extendRegExp(startRegex, "", "$"); 115 | 116 | this.before = this.before.replace(regex, 117 | function (match) { 118 | chunkObj.startTag = chunkObj.startTag + match; 119 | return ""; 120 | }); 121 | 122 | regex = util.extendRegExp(startRegex, "^", ""); 123 | 124 | this.selection = this.selection.replace(regex, 125 | function (match) { 126 | chunkObj.startTag = chunkObj.startTag + match; 127 | return ""; 128 | }); 129 | } 130 | 131 | if (endRegex) { 132 | 133 | regex = util.extendRegExp(endRegex, "", "$"); 134 | 135 | this.selection = this.selection.replace(regex, 136 | function (match) { 137 | chunkObj.endTag = match + chunkObj.endTag; 138 | return ""; 139 | }); 140 | 141 | regex = util.extendRegExp(endRegex, "^", ""); 142 | 143 | this.after = this.after.replace(regex, 144 | function (match) { 145 | chunkObj.endTag = match + chunkObj.endTag; 146 | return ""; 147 | }); 148 | } 149 | }; 150 | 151 | // If remove is false, the whitespace is transferred 152 | // to the before/after regions. 153 | // 154 | // If remove is true, the whitespace disappears. 155 | Chunks.prototype.trimWhitespace = function (remove) { 156 | var beforeReplacer, afterReplacer, that = this; 157 | if (remove) { 158 | beforeReplacer = afterReplacer = ""; 159 | } else { 160 | beforeReplacer = function (s) { that.before += s; return ""; } 161 | afterReplacer = function (s) { that.after = s + that.after; return ""; } 162 | } 163 | 164 | this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); 165 | }; 166 | 167 | 168 | Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) { 169 | 170 | if (nLinesBefore === undefined) { 171 | nLinesBefore = 1; 172 | } 173 | 174 | if (nLinesAfter === undefined) { 175 | nLinesAfter = 1; 176 | } 177 | 178 | nLinesBefore++; 179 | nLinesAfter++; 180 | 181 | var regexText; 182 | var replacementText; 183 | 184 | // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985 185 | if (navigator.userAgent.match(/Chrome/)) { 186 | "X".match(/()./); 187 | } 188 | 189 | this.selection = this.selection.replace(/(^\n*)/, ""); 190 | 191 | this.startTag = this.startTag + re.$1; 192 | 193 | this.selection = this.selection.replace(/(\n*$)/, ""); 194 | this.endTag = this.endTag + re.$1; 195 | this.startTag = this.startTag.replace(/(^\n*)/, ""); 196 | this.before = this.before + re.$1; 197 | this.endTag = this.endTag.replace(/(\n*$)/, ""); 198 | this.after = this.after + re.$1; 199 | 200 | if (this.before) { 201 | 202 | regexText = replacementText = ""; 203 | 204 | while (nLinesBefore--) { 205 | regexText += "\\n?"; 206 | replacementText += "\n"; 207 | } 208 | 209 | if (findExtraNewlines) { 210 | regexText = "\\n*"; 211 | } 212 | this.before = this.before.replace(new re(regexText + "$", ""), replacementText); 213 | } 214 | 215 | if (this.after) { 216 | 217 | regexText = replacementText = ""; 218 | 219 | while (nLinesAfter--) { 220 | regexText += "\\n?"; 221 | replacementText += "\n"; 222 | } 223 | if (findExtraNewlines) { 224 | regexText = "\\n*"; 225 | } 226 | 227 | this.after = this.after.replace(new re(regexText, ""), replacementText); 228 | } 229 | }; 230 | 231 | // end of Chunks 232 | 233 | // A collection of the important regions on the page. 234 | // Cached so we don't have to keep traversing the DOM. 235 | // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around 236 | // this issue: 237 | // Internet explorer has problems with CSS sprite buttons that use HTML 238 | // lists. When you click on the background image "button", IE will 239 | // select the non-existent link text and discard the selection in the 240 | // textarea. The solution to this is to cache the textarea selection 241 | // on the button's mousedown event and set a flag. In the part of the 242 | // code where we need to grab the selection, we check for the flag 243 | // and, if it's set, use the cached area instead of querying the 244 | // textarea. 245 | // 246 | // This ONLY affects Internet Explorer (tested on versions 6, 7 247 | // and 8) and ONLY on button clicks. Keyboard shortcuts work 248 | // normally since the focus never leaves the textarea. 249 | function PanelCollection(postfix) { 250 | this.buttonBar = doc.getElementById("wmd-button-bar" + postfix); 251 | this.preview = doc.getElementById("wmd-preview" + postfix); 252 | this.input = doc.getElementById("wmd-input" + postfix); 253 | }; 254 | 255 | // Returns true if the DOM element is visible, false if it's hidden. 256 | // Checks if display is anything other than none. 257 | util.isVisible = function (elem) { 258 | 259 | if (window.getComputedStyle) { 260 | // Most browsers 261 | return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none"; 262 | } 263 | else if (elem.currentStyle) { 264 | // IE 265 | return elem.currentStyle["display"] !== "none"; 266 | } 267 | }; 268 | 269 | 270 | // Adds a listener callback to a DOM element which is fired on a specified 271 | // event. 272 | util.addEvent = function (elem, event, listener) { 273 | if (elem.attachEvent) { 274 | // IE only. The "on" is mandatory. 275 | elem.attachEvent("on" + event, listener); 276 | } 277 | else { 278 | // Other browsers. 279 | elem.addEventListener(event, listener, false); 280 | } 281 | }; 282 | 283 | 284 | // Removes a listener callback from a DOM element which is fired on a specified 285 | // event. 286 | util.removeEvent = function (elem, event, listener) { 287 | if (elem.detachEvent) { 288 | // IE only. The "on" is mandatory. 289 | elem.detachEvent("on" + event, listener); 290 | } 291 | else { 292 | // Other browsers. 293 | elem.removeEventListener(event, listener, false); 294 | } 295 | }; 296 | 297 | // Converts \r\n and \r to \n. 298 | util.fixEolChars = function (text) { 299 | text = text.replace(/\r\n/g, "\n"); 300 | text = text.replace(/\r/g, "\n"); 301 | return text; 302 | }; 303 | 304 | // Extends a regular expression. Returns a new RegExp 305 | // using pre + regex + post as the expression. 306 | // Used in a few functions where we have a base 307 | // expression and we want to pre- or append some 308 | // conditions to it (e.g. adding "$" to the end). 309 | // The flags are unchanged. 310 | // 311 | // regex is a RegExp, pre and post are strings. 312 | util.extendRegExp = function (regex, pre, post) { 313 | 314 | if (pre === null || pre === undefined) { 315 | pre = ""; 316 | } 317 | if (post === null || post === undefined) { 318 | post = ""; 319 | } 320 | 321 | var pattern = regex.toString(); 322 | var flags; 323 | 324 | // Replace the flags with empty space and store them. 325 | pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) { 326 | flags = flagsPart; 327 | return ""; 328 | }); 329 | 330 | // Remove the slash delimiters on the regular expression. 331 | pattern = pattern.replace(/(^\/|\/$)/g, ""); 332 | pattern = pre + pattern + post; 333 | 334 | return new re(pattern, flags); 335 | } 336 | 337 | // UNFINISHED 338 | // The assignment in the while loop makes jslint cranky. 339 | // I'll change it to a better loop later. 340 | position.getTop = function (elem, isInner) { 341 | var result = elem.offsetTop; 342 | if (!isInner) { 343 | while (elem = elem.offsetParent) { 344 | result += elem.offsetTop; 345 | } 346 | } 347 | return result; 348 | }; 349 | 350 | position.getHeight = function (elem) { 351 | return elem.offsetHeight || elem.scrollHeight; 352 | }; 353 | 354 | position.getWidth = function (elem) { 355 | return elem.offsetWidth || elem.scrollWidth; 356 | }; 357 | 358 | position.getPageSize = function () { 359 | 360 | var scrollWidth, scrollHeight; 361 | var innerWidth, innerHeight; 362 | 363 | // It's not very clear which blocks work with which browsers. 364 | if (self.innerHeight && self.scrollMaxY) { 365 | scrollWidth = doc.body.scrollWidth; 366 | scrollHeight = self.innerHeight + self.scrollMaxY; 367 | } 368 | else if (doc.body.scrollHeight > doc.body.offsetHeight) { 369 | scrollWidth = doc.body.scrollWidth; 370 | scrollHeight = doc.body.scrollHeight; 371 | } 372 | else { 373 | scrollWidth = doc.body.offsetWidth; 374 | scrollHeight = doc.body.offsetHeight; 375 | } 376 | 377 | if (self.innerHeight) { 378 | // Non-IE browser 379 | innerWidth = self.innerWidth; 380 | innerHeight = self.innerHeight; 381 | } 382 | else if (doc.documentElement && doc.documentElement.clientHeight) { 383 | // Some versions of IE (IE 6 w/ a DOCTYPE declaration) 384 | innerWidth = doc.documentElement.clientWidth; 385 | innerHeight = doc.documentElement.clientHeight; 386 | } 387 | else if (doc.body) { 388 | // Other versions of IE 389 | innerWidth = doc.body.clientWidth; 390 | innerHeight = doc.body.clientHeight; 391 | } 392 | 393 | var maxWidth = Math.max(scrollWidth, innerWidth); 394 | var maxHeight = Math.max(scrollHeight, innerHeight); 395 | return [maxWidth, maxHeight, innerWidth, innerHeight]; 396 | }; 397 | 398 | // Handles pushing and popping TextareaStates for undo/redo commands. 399 | // I should rename the stack variables to list. 400 | function UndoManager(callback, panels) { 401 | 402 | var undoObj = this; 403 | var undoStack = []; // A stack of undo states 404 | var stackPtr = 0; // The index of the current state 405 | var mode = "none"; 406 | var lastState; // The last state 407 | var timer; // The setTimeout handle for cancelling the timer 408 | var inputStateObj; 409 | 410 | // Set the mode for later logic steps. 411 | var setMode = function (newMode, noSave) { 412 | if (mode != newMode) { 413 | mode = newMode; 414 | if (!noSave) { 415 | saveState(); 416 | } 417 | } 418 | 419 | if (!uaSniffed.isIE || mode != "moving") { 420 | timer = setTimeout(refreshState, 1); 421 | } 422 | else { 423 | inputStateObj = null; 424 | } 425 | }; 426 | 427 | var refreshState = function (isInitialState) { 428 | inputStateObj = new TextareaState(panels, isInitialState); 429 | timer = undefined; 430 | }; 431 | 432 | this.setCommandMode = function () { 433 | mode = "command"; 434 | saveState(); 435 | timer = setTimeout(refreshState, 0); 436 | }; 437 | 438 | this.canUndo = function () { 439 | return stackPtr > 1; 440 | }; 441 | 442 | this.canRedo = function () { 443 | if (undoStack[stackPtr + 1]) { 444 | return true; 445 | } 446 | return false; 447 | }; 448 | 449 | // Removes the last state and restores it. 450 | this.undo = function () { 451 | 452 | if (undoObj.canUndo()) { 453 | if (lastState) { 454 | // What about setting state -1 to null or checking for undefined? 455 | lastState.restore(); 456 | lastState = null; 457 | } 458 | else { 459 | undoStack[stackPtr] = new TextareaState(panels); 460 | undoStack[--stackPtr].restore(); 461 | 462 | if (callback) { 463 | callback(); 464 | } 465 | } 466 | } 467 | 468 | mode = "none"; 469 | panels.input.focus(); 470 | refreshState(); 471 | }; 472 | 473 | // Redo an action. 474 | this.redo = function () { 475 | 476 | if (undoObj.canRedo()) { 477 | 478 | undoStack[++stackPtr].restore(); 479 | 480 | if (callback) { 481 | callback(); 482 | } 483 | } 484 | 485 | mode = "none"; 486 | panels.input.focus(); 487 | refreshState(); 488 | }; 489 | 490 | // Push the input area state to the stack. 491 | var saveState = function () { 492 | var currState = inputStateObj || new TextareaState(panels); 493 | 494 | if (!currState) { 495 | return false; 496 | } 497 | if (mode == "moving") { 498 | if (!lastState) { 499 | lastState = currState; 500 | } 501 | return; 502 | } 503 | if (lastState) { 504 | if (undoStack[stackPtr - 1].text != lastState.text) { 505 | undoStack[stackPtr++] = lastState; 506 | } 507 | lastState = null; 508 | } 509 | undoStack[stackPtr++] = currState; 510 | undoStack[stackPtr + 1] = null; 511 | if (callback) { 512 | callback(); 513 | } 514 | }; 515 | 516 | var handleCtrlYZ = function (event) { 517 | 518 | var handled = false; 519 | 520 | if (event.ctrlKey || event.metaKey) { 521 | 522 | // IE and Opera do not support charCode. 523 | var keyCode = event.charCode || event.keyCode; 524 | var keyCodeChar = String.fromCharCode(keyCode); 525 | 526 | switch (keyCodeChar) { 527 | 528 | case "y": 529 | undoObj.redo(); 530 | handled = true; 531 | break; 532 | 533 | case "z": 534 | if (!event.shiftKey) { 535 | undoObj.undo(); 536 | } 537 | else { 538 | undoObj.redo(); 539 | } 540 | handled = true; 541 | break; 542 | } 543 | } 544 | 545 | if (handled) { 546 | if (event.preventDefault) { 547 | event.preventDefault(); 548 | } 549 | if (window.event) { 550 | window.event.returnValue = false; 551 | } 552 | return; 553 | } 554 | }; 555 | 556 | // Set the mode depending on what is going on in the input area. 557 | var handleModeChange = function (event) { 558 | 559 | if (!event.ctrlKey && !event.metaKey) { 560 | 561 | var keyCode = event.keyCode; 562 | 563 | if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) { 564 | // 33 - 40: page up/dn and arrow keys 565 | // 63232 - 63235: page up/dn and arrow keys on safari 566 | setMode("moving"); 567 | } 568 | else if (keyCode == 8 || keyCode == 46 || keyCode == 127) { 569 | // 8: backspace 570 | // 46: delete 571 | // 127: delete 572 | setMode("deleting"); 573 | } 574 | else if (keyCode == 13) { 575 | // 13: Enter 576 | setMode("newlines"); 577 | } 578 | else if (keyCode == 27) { 579 | // 27: escape 580 | setMode("escape"); 581 | } 582 | else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) { 583 | // 16-20 are shift, etc. 584 | // 91: left window key 585 | // I think this might be a little messed up since there are 586 | // a lot of nonprinting keys above 20. 587 | setMode("typing"); 588 | } 589 | } 590 | }; 591 | 592 | var setEventHandlers = function () { 593 | util.addEvent(panels.input, "keypress", function (event) { 594 | // keyCode 89: y 595 | // keyCode 90: z 596 | if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) { 597 | event.preventDefault(); 598 | } 599 | }); 600 | 601 | var handlePaste = function () { 602 | if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) { 603 | if (timer == undefined) { 604 | mode = "paste"; 605 | saveState(); 606 | refreshState(); 607 | } 608 | } 609 | }; 610 | 611 | util.addEvent(panels.input, "keydown", handleCtrlYZ); 612 | util.addEvent(panels.input, "keydown", handleModeChange); 613 | util.addEvent(panels.input, "mousedown", function () { 614 | setMode("moving"); 615 | }); 616 | 617 | panels.input.onpaste = handlePaste; 618 | panels.input.ondrop = handlePaste; 619 | }; 620 | 621 | var init = function () { 622 | setEventHandlers(); 623 | refreshState(true); 624 | saveState(); 625 | }; 626 | 627 | init(); 628 | } 629 | 630 | // end of UndoManager 631 | 632 | // The input textarea state/contents. 633 | // This is used to implement undo/redo by the undo manager. 634 | function TextareaState(panels, isInitialState) { 635 | 636 | // Aliases 637 | var stateObj = this; 638 | var inputArea = panels.input; 639 | this.init = function () { 640 | if (!util.isVisible(inputArea)) { 641 | return; 642 | } 643 | if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box 644 | return; 645 | } 646 | 647 | this.setInputAreaSelectionStartEnd(); 648 | this.scrollTop = inputArea.scrollTop; 649 | if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { 650 | this.text = inputArea.value; 651 | } 652 | 653 | } 654 | 655 | // Sets the selected text in the input box after we've performed an 656 | // operation. 657 | this.setInputAreaSelection = function () { 658 | 659 | if (!util.isVisible(inputArea)) { 660 | return; 661 | } 662 | 663 | if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { 664 | 665 | inputArea.focus(); 666 | inputArea.selectionStart = stateObj.start; 667 | inputArea.selectionEnd = stateObj.end; 668 | inputArea.scrollTop = stateObj.scrollTop; 669 | } 670 | else if (doc.selection) { 671 | 672 | if (doc.activeElement && doc.activeElement !== inputArea) { 673 | return; 674 | } 675 | 676 | inputArea.focus(); 677 | var range = inputArea.createTextRange(); 678 | range.moveStart("character", -inputArea.value.length); 679 | range.moveEnd("character", -inputArea.value.length); 680 | range.moveEnd("character", stateObj.end); 681 | range.moveStart("character", stateObj.start); 682 | range.select(); 683 | } 684 | }; 685 | 686 | this.setInputAreaSelectionStartEnd = function () { 687 | 688 | if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { 689 | 690 | stateObj.start = inputArea.selectionStart; 691 | stateObj.end = inputArea.selectionEnd; 692 | } 693 | else if (doc.selection) { 694 | 695 | stateObj.text = util.fixEolChars(inputArea.value); 696 | 697 | // IE loses the selection in the textarea when buttons are 698 | // clicked. On IE we cache the selection. Here, if something is cached, 699 | // we take it. 700 | var range = panels.ieCachedRange || doc.selection.createRange(); 701 | 702 | var fixedRange = util.fixEolChars(range.text); 703 | var marker = "\x07"; 704 | var markedRange = marker + fixedRange + marker; 705 | range.text = markedRange; 706 | var inputText = util.fixEolChars(inputArea.value); 707 | 708 | range.moveStart("character", -markedRange.length); 709 | range.text = fixedRange; 710 | 711 | stateObj.start = inputText.indexOf(marker); 712 | stateObj.end = inputText.lastIndexOf(marker) - marker.length; 713 | 714 | var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; 715 | 716 | if (len) { 717 | range.moveStart("character", -fixedRange.length); 718 | while (len--) { 719 | fixedRange += "\n"; 720 | stateObj.end += 1; 721 | } 722 | range.text = fixedRange; 723 | } 724 | 725 | if (panels.ieCachedRange) 726 | stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange 727 | 728 | panels.ieCachedRange = null; 729 | 730 | this.setInputAreaSelection(); 731 | } 732 | }; 733 | 734 | // Restore this state into the input area. 735 | this.restore = function () { 736 | 737 | if (stateObj.text != undefined && stateObj.text != inputArea.value) { 738 | inputArea.value = stateObj.text; 739 | } 740 | this.setInputAreaSelection(); 741 | inputArea.scrollTop = stateObj.scrollTop; 742 | }; 743 | 744 | // Gets a collection of HTML chunks from the inptut textarea. 745 | this.getChunks = function () { 746 | 747 | var chunk = new Chunks(); 748 | chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); 749 | chunk.startTag = ""; 750 | chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); 751 | chunk.endTag = ""; 752 | chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); 753 | chunk.scrollTop = stateObj.scrollTop; 754 | 755 | return chunk; 756 | }; 757 | 758 | // Sets the TextareaState properties given a chunk of markdown. 759 | this.setChunks = function (chunk) { 760 | 761 | chunk.before = chunk.before + chunk.startTag; 762 | chunk.after = chunk.endTag + chunk.after; 763 | 764 | this.start = chunk.before.length; 765 | this.end = chunk.before.length + chunk.selection.length; 766 | this.text = chunk.before + chunk.selection + chunk.after; 767 | this.scrollTop = chunk.scrollTop; 768 | }; 769 | this.init(); 770 | }; 771 | 772 | function PreviewManager(converter, panels, previewRefreshCallback) { 773 | 774 | var managerObj = this; 775 | var timeout; 776 | var elapsedTime; 777 | var oldInputText; 778 | var maxDelay = 3000; 779 | var startType = "delayed"; // The other legal value is "manual" 780 | 781 | // Adds event listeners to elements 782 | var setupEvents = function (inputElem, listener) { 783 | 784 | util.addEvent(inputElem, "input", listener); 785 | inputElem.onpaste = listener; 786 | inputElem.ondrop = listener; 787 | 788 | util.addEvent(inputElem, "keypress", listener); 789 | util.addEvent(inputElem, "keydown", listener); 790 | }; 791 | 792 | var getDocScrollTop = function () { 793 | 794 | var result = 0; 795 | 796 | if (window.innerHeight) { 797 | result = window.pageYOffset; 798 | } 799 | else 800 | if (doc.documentElement && doc.documentElement.scrollTop) { 801 | result = doc.documentElement.scrollTop; 802 | } 803 | else 804 | if (doc.body) { 805 | result = doc.body.scrollTop; 806 | } 807 | 808 | return result; 809 | }; 810 | 811 | var makePreviewHtml = function () { 812 | 813 | // If there is no registered preview panel 814 | // there is nothing to do. 815 | if (!panels.preview) 816 | return; 817 | 818 | 819 | var text = panels.input.value; 820 | if (text && text == oldInputText) { 821 | return; // Input text hasn't changed. 822 | } 823 | else { 824 | oldInputText = text; 825 | } 826 | 827 | var prevTime = new Date().getTime(); 828 | 829 | text = converter.makeHtml(text); 830 | 831 | // Calculate the processing time of the HTML creation. 832 | // It's used as the delay time in the event listener. 833 | var currTime = new Date().getTime(); 834 | elapsedTime = currTime - prevTime; 835 | 836 | pushPreviewHtml(text); 837 | }; 838 | 839 | // setTimeout is already used. Used as an event listener. 840 | var applyTimeout = function () { 841 | 842 | if (timeout) { 843 | clearTimeout(timeout); 844 | timeout = undefined; 845 | } 846 | 847 | if (startType !== "manual") { 848 | 849 | var delay = 0; 850 | 851 | if (startType === "delayed") { 852 | delay = elapsedTime; 853 | } 854 | 855 | if (delay > maxDelay) { 856 | delay = maxDelay; 857 | } 858 | timeout = setTimeout(makePreviewHtml, delay); 859 | } 860 | }; 861 | 862 | var getScaleFactor = function (panel) { 863 | if (panel.scrollHeight <= panel.clientHeight) { 864 | return 1; 865 | } 866 | return panel.scrollTop / (panel.scrollHeight - panel.clientHeight); 867 | }; 868 | 869 | var setPanelScrollTops = function () { 870 | if (panels.preview) { 871 | panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview); 872 | } 873 | }; 874 | 875 | this.refresh = function (requiresRefresh) { 876 | 877 | if (requiresRefresh) { 878 | oldInputText = ""; 879 | makePreviewHtml(); 880 | } 881 | else { 882 | applyTimeout(); 883 | } 884 | }; 885 | 886 | this.processingTime = function () { 887 | return elapsedTime; 888 | }; 889 | 890 | var isFirstTimeFilled = true; 891 | 892 | // IE doesn't let you use innerHTML if the element is contained somewhere in a table 893 | // (which is the case for inline editing) -- in that case, detach the element, set the 894 | // value, and reattach. Yes, that *is* ridiculous. 895 | var ieSafePreviewSet = function (text) { 896 | var preview = panels.preview; 897 | var parent = preview.parentNode; 898 | var sibling = preview.nextSibling; 899 | parent.removeChild(preview); 900 | preview.innerHTML = text; 901 | if (!sibling) 902 | parent.appendChild(preview); 903 | else 904 | parent.insertBefore(preview, sibling); 905 | } 906 | 907 | var nonSuckyBrowserPreviewSet = function (text) { 908 | panels.preview.innerHTML = text; 909 | } 910 | 911 | var previewSetter; 912 | 913 | var previewSet = function (text) { 914 | if (previewSetter) 915 | return previewSetter(text); 916 | 917 | try { 918 | nonSuckyBrowserPreviewSet(text); 919 | previewSetter = nonSuckyBrowserPreviewSet; 920 | } catch (e) { 921 | previewSetter = ieSafePreviewSet; 922 | previewSetter(text); 923 | } 924 | }; 925 | 926 | var pushPreviewHtml = function (text) { 927 | 928 | var emptyTop = position.getTop(panels.input) - getDocScrollTop(); 929 | 930 | if (panels.preview) { 931 | previewSet(text); 932 | previewRefreshCallback(); 933 | } 934 | 935 | setPanelScrollTops(); 936 | 937 | if (isFirstTimeFilled) { 938 | isFirstTimeFilled = false; 939 | return; 940 | } 941 | 942 | var fullTop = position.getTop(panels.input) - getDocScrollTop(); 943 | 944 | if (uaSniffed.isIE) { 945 | setTimeout(function () { 946 | window.scrollBy(0, fullTop - emptyTop); 947 | }, 0); 948 | } 949 | else { 950 | window.scrollBy(0, fullTop - emptyTop); 951 | } 952 | }; 953 | 954 | var init = function () { 955 | 956 | setupEvents(panels.input, applyTimeout); 957 | makePreviewHtml(); 958 | 959 | if (panels.preview) { 960 | panels.preview.scrollTop = 0; 961 | } 962 | }; 963 | 964 | init(); 965 | }; 966 | 967 | 968 | // This simulates a modal dialog box and asks for the URL when you 969 | // click the hyperlink or image buttons. 970 | // 971 | // text: The html for the input box. 972 | // defaultInputText: The default value that appears in the input box. 973 | // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. 974 | // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel 975 | // was chosen). 976 | ui.prompt = function (title, text, defaultInputText, callback) { 977 | 978 | // These variables need to be declared at this level since they are used 979 | // in multiple functions. 980 | var dialog; // The dialog box. 981 | var input; // The text box where you enter the hyperlink. 982 | 983 | 984 | if (defaultInputText === undefined) { 985 | defaultInputText = ""; 986 | } 987 | 988 | // Used as a keydown event handler. Esc dismisses the prompt. 989 | // Key code 27 is ESC. 990 | var checkEscape = function (key) { 991 | var code = (key.charCode || key.keyCode); 992 | if (code === 27) { 993 | close(true); 994 | } 995 | }; 996 | 997 | // Dismisses the hyperlink input box. 998 | // isCancel is true if we don't care about the input text. 999 | // isCancel is false if we are going to keep the text. 1000 | var close = function (isCancel) { 1001 | util.removeEvent(doc.body, "keydown", checkEscape); 1002 | var text = input.value; 1003 | 1004 | if (isCancel) { 1005 | text = null; 1006 | } 1007 | else { 1008 | // Fixes common pasting errors. 1009 | text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); 1010 | if (!/^(?:https?|ftp):\/\//.test(text)) 1011 | text = 'http://' + text; 1012 | } 1013 | 1014 | $(dialog).modal('hide'); 1015 | 1016 | callback(text); 1017 | return false; 1018 | }; 1019 | 1020 | 1021 | 1022 | // Create the text input box form/window. 1023 | var createDialog = function () { 1024 | // 1037 | 1038 | // The main dialog box. 1039 | dialog = doc.createElement("div"); 1040 | dialog.className = "modal fade"; 1041 | 1042 | // The modal-dialog div. 1043 | var div_dialog = doc.createElement("div"); 1044 | div_dialog.className = "modal-dialog"; 1045 | dialog.appendChild(div_dialog); 1046 | 1047 | // The modal-content div. 1048 | var content = doc.createElement("div"); 1049 | content.className = "modal-content"; 1050 | div_dialog.appendChild(content); 1051 | 1052 | // The header. 1053 | var header = doc.createElement("div"); 1054 | header.className = "modal-header"; 1055 | header.innerHTML = '× '; 1056 | content.appendChild(header); 1057 | 1058 | // The body. 1059 | var body = doc.createElement("div"); 1060 | body.className = "modal-body"; 1061 | content.appendChild(body); 1062 | 1063 | // The footer. 1064 | var footer = doc.createElement("div"); 1065 | footer.className = "modal-footer"; 1066 | content.appendChild(footer); 1067 | 1068 | // The dialog text. 1069 | var question = doc.createElement("p"); 1070 | question.innerHTML = text; 1071 | question.style.padding = "5px"; 1072 | body.appendChild(question); 1073 | 1074 | // The web form container for the text box and buttons. 1075 | var form = doc.createElement("form"), 1076 | style = form.style; 1077 | form.onsubmit = function () { return close(false); }; 1078 | style.padding = "0"; 1079 | style.margin = "0"; 1080 | body.appendChild(form); 1081 | 1082 | // The input text box 1083 | input = doc.createElement("input"); 1084 | input.type = "text"; 1085 | input.value = defaultInputText; 1086 | input.className = "form-control"; 1087 | style = input.style; 1088 | style.display = "block"; 1089 | style.width = "80%"; 1090 | style.marginLeft = style.marginRight = "auto"; 1091 | form.appendChild(input); 1092 | 1093 | // The ok button 1094 | var okButton = doc.createElement("button"); 1095 | okButton.className = "btn btn-primary"; 1096 | okButton.type = "button"; 1097 | okButton.onclick = function () { return close(false); }; 1098 | okButton.innerHTML = "OK"; 1099 | 1100 | // The cancel button 1101 | var cancelButton = doc.createElement("button"); 1102 | cancelButton.className = "btn btn-danger"; 1103 | cancelButton.type = "button"; 1104 | cancelButton.onclick = function () { return close(true); }; 1105 | cancelButton.innerHTML = "Cancel"; 1106 | 1107 | footer.appendChild(okButton); 1108 | footer.appendChild(cancelButton); 1109 | 1110 | util.addEvent(doc.body, "keydown", checkEscape); 1111 | 1112 | doc.body.appendChild(dialog); 1113 | 1114 | }; 1115 | 1116 | // Why is this in a zero-length timeout? 1117 | // Is it working around a browser bug? 1118 | setTimeout(function () { 1119 | 1120 | createDialog(); 1121 | 1122 | var defTextLen = defaultInputText.length; 1123 | if (input.selectionStart !== undefined) { 1124 | input.selectionStart = 0; 1125 | input.selectionEnd = defTextLen; 1126 | } 1127 | else if (input.createTextRange) { 1128 | var range = input.createTextRange(); 1129 | range.collapse(false); 1130 | range.moveStart("character", -defTextLen); 1131 | range.moveEnd("character", defTextLen); 1132 | range.select(); 1133 | } 1134 | 1135 | $(dialog).on('shown', function () { 1136 | input.focus(); 1137 | }) 1138 | 1139 | $(dialog).on('hidden', function () { 1140 | dialog.parentNode.removeChild(dialog); 1141 | }) 1142 | 1143 | $(dialog).modal() 1144 | 1145 | }, 0); 1146 | }; 1147 | 1148 | function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) { 1149 | 1150 | var inputBox = panels.input, 1151 | buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements. 1152 | 1153 | makeSpritedButtonRow(); 1154 | 1155 | var keyEvent = "keydown"; 1156 | if (uaSniffed.isOpera) { 1157 | keyEvent = "keypress"; 1158 | } 1159 | 1160 | util.addEvent(inputBox, keyEvent, function (key) { 1161 | 1162 | // Check to see if we have a button key and, if so execute the callback. 1163 | if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { 1164 | 1165 | var keyCode = key.charCode || key.keyCode; 1166 | var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); 1167 | 1168 | switch (keyCodeStr) { 1169 | case "b": 1170 | doClick(buttons.bold); 1171 | break; 1172 | case "i": 1173 | doClick(buttons.italic); 1174 | break; 1175 | case "l": 1176 | doClick(buttons.link); 1177 | break; 1178 | case "q": 1179 | doClick(buttons.quote); 1180 | break; 1181 | case "k": 1182 | doClick(buttons.code); 1183 | break; 1184 | case "g": 1185 | doClick(buttons.image); 1186 | break; 1187 | case "o": 1188 | doClick(buttons.olist); 1189 | break; 1190 | case "u": 1191 | doClick(buttons.ulist); 1192 | break; 1193 | case "h": 1194 | doClick(buttons.heading); 1195 | break; 1196 | case "r": 1197 | doClick(buttons.hr); 1198 | break; 1199 | case "y": 1200 | doClick(buttons.redo); 1201 | break; 1202 | case "z": 1203 | if (key.shiftKey) { 1204 | doClick(buttons.redo); 1205 | } 1206 | else { 1207 | doClick(buttons.undo); 1208 | } 1209 | break; 1210 | default: 1211 | return; 1212 | } 1213 | 1214 | 1215 | if (key.preventDefault) { 1216 | key.preventDefault(); 1217 | } 1218 | 1219 | if (window.event) { 1220 | window.event.returnValue = false; 1221 | } 1222 | } 1223 | }); 1224 | 1225 | // Auto-indent on shift-enter 1226 | util.addEvent(inputBox, "keyup", function (key) { 1227 | if (key.shiftKey && !key.ctrlKey && !key.metaKey) { 1228 | var keyCode = key.charCode || key.keyCode; 1229 | // Character 13 is Enter 1230 | if (keyCode === 13) { 1231 | var fakeButton = {}; 1232 | fakeButton.textOp = bindCommand("doAutoindent"); 1233 | doClick(fakeButton); 1234 | } 1235 | } 1236 | }); 1237 | 1238 | // special handler because IE clears the context of the textbox on ESC 1239 | if (uaSniffed.isIE) { 1240 | util.addEvent(inputBox, "keydown", function (key) { 1241 | var code = key.keyCode; 1242 | if (code === 27) { 1243 | return false; 1244 | } 1245 | }); 1246 | } 1247 | 1248 | 1249 | // Perform the button's action. 1250 | function doClick(button) { 1251 | 1252 | inputBox.focus(); 1253 | 1254 | if (button.textOp) { 1255 | 1256 | if (undoManager) { 1257 | undoManager.setCommandMode(); 1258 | } 1259 | 1260 | var state = new TextareaState(panels); 1261 | 1262 | if (!state) { 1263 | return; 1264 | } 1265 | 1266 | var chunks = state.getChunks(); 1267 | 1268 | // Some commands launch a "modal" prompt dialog. Javascript 1269 | // can't really make a modal dialog box and the WMD code 1270 | // will continue to execute while the dialog is displayed. 1271 | // This prevents the dialog pattern I'm used to and means 1272 | // I can't do something like this: 1273 | // 1274 | // var link = CreateLinkDialog(); 1275 | // makeMarkdownLink(link); 1276 | // 1277 | // Instead of this straightforward method of handling a 1278 | // dialog I have to pass any code which would execute 1279 | // after the dialog is dismissed (e.g. link creation) 1280 | // in a function parameter. 1281 | // 1282 | // Yes this is awkward and I think it sucks, but there's 1283 | // no real workaround. Only the image and link code 1284 | // create dialogs and require the function pointers. 1285 | var fixupInputArea = function () { 1286 | 1287 | inputBox.focus(); 1288 | 1289 | if (chunks) { 1290 | state.setChunks(chunks); 1291 | } 1292 | 1293 | state.restore(); 1294 | previewManager.refresh(); 1295 | }; 1296 | 1297 | var noCleanup = button.textOp(chunks, fixupInputArea); 1298 | 1299 | if (!noCleanup) { 1300 | fixupInputArea(); 1301 | } 1302 | 1303 | } 1304 | 1305 | if (button.execute) { 1306 | button.execute(undoManager); 1307 | } 1308 | }; 1309 | 1310 | function setupButton(button, isEnabled) { 1311 | 1312 | if (isEnabled) { 1313 | button.disabled = false; 1314 | 1315 | if (!button.isHelp) { 1316 | button.onclick = function () { 1317 | if (this.onmouseout) { 1318 | this.onmouseout(); 1319 | } 1320 | doClick(this); 1321 | return false; 1322 | } 1323 | } 1324 | } 1325 | else { 1326 | button.disabled = true; 1327 | } 1328 | } 1329 | 1330 | function bindCommand(method) { 1331 | if (typeof method === "string") 1332 | method = commandManager[method]; 1333 | return function () { method.apply(commandManager, arguments); } 1334 | } 1335 | 1336 | function makeSpritedButtonRow() { 1337 | 1338 | var buttonBar = panels.buttonBar; 1339 | var buttonRow = document.createElement("div"); 1340 | buttonRow.id = "wmd-button-row" + postfix; 1341 | buttonRow.className = 'btn-toolbar'; 1342 | buttonRow = buttonBar.appendChild(buttonRow); 1343 | 1344 | var makeButton = function (id, title, icon, textOp, group) { 1345 | var button = document.createElement("button"); 1346 | button.className = "btn btn-default"; 1347 | var buttonImage = document.createElement("i"); 1348 | buttonImage.className = icon; 1349 | button.id = id + postfix; 1350 | button.appendChild(buttonImage); 1351 | button.title = title; 1352 | $(button).tooltip({placement: 'bottom', container: 'body'}) 1353 | if (textOp) 1354 | button.textOp = textOp; 1355 | setupButton(button, true); 1356 | if (group) { 1357 | group.appendChild(button); 1358 | } else { 1359 | buttonRow.appendChild(button); 1360 | } 1361 | return button; 1362 | }; 1363 | var makeGroup = function (num) { 1364 | var group = document.createElement("div"); 1365 | group.className = "btn-group wmd-button-group" + num; 1366 | group.id = "wmd-button-group" + num + postfix; 1367 | buttonRow.appendChild(group); 1368 | return group 1369 | } 1370 | 1371 | group1 = makeGroup(1); 1372 | buttons.bold = makeButton("wmd-bold-button", "Bold - Ctrl+B", "fa fa-bold", bindCommand("doBold"), group1); 1373 | buttons.italic = makeButton("wmd-italic-button", "Italic - Ctrl+I", "fa fa-italic", bindCommand("doItalic"), group1); 1374 | 1375 | group2 = makeGroup(2); 1376 | buttons.link = makeButton("wmd-link-button", "Link - Ctrl+L", "fa fa-link", bindCommand(function (chunk, postProcessing) { 1377 | return this.doLinkOrImage(chunk, postProcessing, false); 1378 | }), group2); 1379 | buttons.quote = makeButton("wmd-quote-button", "Blockquote - Ctrl+Q", "fa fa-quote-left", bindCommand("doBlockquote"), group2); 1380 | buttons.code = makeButton("wmd-code-button", "Code Sample - Ctrl+K", "fa fa-code", bindCommand("doCode"), group2); 1381 | buttons.image = makeButton("wmd-image-button", "Image - Ctrl+G", "fa fa-picture-o", bindCommand(function (chunk, postProcessing) { 1382 | return this.doLinkOrImage(chunk, postProcessing, true); 1383 | }), group2); 1384 | 1385 | group3 = makeGroup(3); 1386 | buttons.olist = makeButton("wmd-olist-button", "Numbered List - Ctrl+O", "fa fa-list-ol", bindCommand(function (chunk, postProcessing) { 1387 | this.doList(chunk, postProcessing, true); 1388 | }), group3); 1389 | buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List - Ctrl+U", "fa fa-list-ul", bindCommand(function (chunk, postProcessing) { 1390 | this.doList(chunk, postProcessing, false); 1391 | }), group3); 1392 | buttons.heading = makeButton("wmd-heading-button", "Heading - Ctrl+H", "fa fa-header", bindCommand("doHeading"), group3); 1393 | buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule - Ctrl+R", "fa fa-ellipsis-h", bindCommand("doHorizontalRule"), group3); 1394 | 1395 | group4 = makeGroup(4); 1396 | buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "fa fa-undo", null, group4); 1397 | buttons.undo.execute = function (manager) { if (manager) manager.undo(); }; 1398 | 1399 | var redoTitle = /win/.test(nav.platform.toLowerCase()) ? 1400 | "Redo - Ctrl+Y" : 1401 | "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms 1402 | 1403 | buttons.redo = makeButton("wmd-redo-button", redoTitle, "fa fa-rotate-right", null, group4); 1404 | buttons.redo.execute = function (manager) { if (manager) manager.redo(); }; 1405 | 1406 | if (helpOptions) { 1407 | group5 = makeGroup(5); 1408 | group5.className = group5.className + " pull-right"; 1409 | var helpButton = document.createElement("button"); 1410 | var helpButtonImage = document.createElement("i"); 1411 | helpButtonImage.className = "fa fa-question"; 1412 | helpButton.appendChild(helpButtonImage); 1413 | helpButton.className = "btn"; 1414 | helpButton.id = "wmd-help-button" + postfix; 1415 | helpButton.isHelp = true; 1416 | helpButton.title = helpOptions.title || defaultHelpHoverTitle; 1417 | $(helpButton).tooltip({placement: 'bottom', container: 'body'}) 1418 | helpButton.onclick = helpOptions.handler; 1419 | 1420 | setupButton(helpButton, true); 1421 | group5.appendChild(helpButton); 1422 | buttons.help = helpButton; 1423 | } 1424 | 1425 | setUndoRedoButtonStates(); 1426 | } 1427 | 1428 | function setUndoRedoButtonStates() { 1429 | if (undoManager) { 1430 | setupButton(buttons.undo, undoManager.canUndo()); 1431 | setupButton(buttons.redo, undoManager.canRedo()); 1432 | } 1433 | }; 1434 | 1435 | this.setUndoRedoButtonStates = setUndoRedoButtonStates; 1436 | 1437 | } 1438 | 1439 | function CommandManager(pluginHooks) { 1440 | this.hooks = pluginHooks; 1441 | } 1442 | 1443 | var commandProto = CommandManager.prototype; 1444 | 1445 | // The markdown symbols - 4 spaces = code, > = blockquote, etc. 1446 | commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)"; 1447 | 1448 | // Remove markdown symbols from the chunk selection. 1449 | commandProto.unwrap = function (chunk) { 1450 | var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g"); 1451 | chunk.selection = chunk.selection.replace(txt, "$1 $2"); 1452 | }; 1453 | 1454 | commandProto.wrap = function (chunk, len) { 1455 | this.unwrap(chunk); 1456 | var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"), 1457 | that = this; 1458 | 1459 | chunk.selection = chunk.selection.replace(regex, function (line, marked) { 1460 | if (new re("^" + that.prefixes, "").test(line)) { 1461 | return line; 1462 | } 1463 | return marked + "\n"; 1464 | }); 1465 | 1466 | chunk.selection = chunk.selection.replace(/\s+$/, ""); 1467 | }; 1468 | 1469 | commandProto.doBold = function (chunk, postProcessing) { 1470 | return this.doBorI(chunk, postProcessing, 2, "strong text"); 1471 | }; 1472 | 1473 | commandProto.doItalic = function (chunk, postProcessing) { 1474 | return this.doBorI(chunk, postProcessing, 1, "emphasized text"); 1475 | }; 1476 | 1477 | // chunk: The selected region that will be enclosed with */** 1478 | // nStars: 1 for italics, 2 for bold 1479 | // insertText: If you just click the button without highlighting text, this gets inserted 1480 | commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) { 1481 | 1482 | // Get rid of whitespace and fixup newlines. 1483 | chunk.trimWhitespace(); 1484 | chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n"); 1485 | 1486 | // Look for stars before and after. Is the chunk already marked up? 1487 | // note that these regex matches cannot fail 1488 | var starsBefore = /(\**$)/.exec(chunk.before)[0]; 1489 | var starsAfter = /(^\**)/.exec(chunk.after)[0]; 1490 | 1491 | var prevStars = Math.min(starsBefore.length, starsAfter.length); 1492 | 1493 | // Remove stars if we have to since the button acts as a toggle. 1494 | if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) { 1495 | chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), ""); 1496 | chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), ""); 1497 | } 1498 | else if (!chunk.selection && starsAfter) { 1499 | // It's not really clear why this code is necessary. It just moves 1500 | // some arbitrary stuff around. 1501 | chunk.after = chunk.after.replace(/^([*_]*)/, ""); 1502 | chunk.before = chunk.before.replace(/(\s?)$/, ""); 1503 | var whitespace = re.$1; 1504 | chunk.before = chunk.before + starsAfter + whitespace; 1505 | } 1506 | else { 1507 | 1508 | // In most cases, if you don't have any selected text and click the button 1509 | // you'll get a selected, marked up region with the default text inserted. 1510 | if (!chunk.selection && !starsAfter) { 1511 | chunk.selection = insertText; 1512 | } 1513 | 1514 | // Add the true markup. 1515 | var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ? 1516 | chunk.before = chunk.before + markup; 1517 | chunk.after = markup + chunk.after; 1518 | } 1519 | 1520 | return; 1521 | }; 1522 | 1523 | commandProto.stripLinkDefs = function (text, defsToAdd) { 1524 | 1525 | text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, 1526 | function (totalMatch, id, link, newlines, title) { 1527 | defsToAdd[id] = totalMatch.replace(/\s*$/, ""); 1528 | if (newlines) { 1529 | // Strip the title and return that separately. 1530 | defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, ""); 1531 | return newlines + title; 1532 | } 1533 | return ""; 1534 | }); 1535 | 1536 | return text; 1537 | }; 1538 | 1539 | commandProto.addLinkDef = function (chunk, linkDef) { 1540 | 1541 | var refNumber = 0; // The current reference number 1542 | var defsToAdd = {}; // 1543 | // Start with a clean slate by removing all previous link definitions. 1544 | chunk.before = this.stripLinkDefs(chunk.before, defsToAdd); 1545 | chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd); 1546 | chunk.after = this.stripLinkDefs(chunk.after, defsToAdd); 1547 | 1548 | var defs = ""; 1549 | var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g; 1550 | 1551 | var addDefNumber = function (def) { 1552 | refNumber++; 1553 | def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:"); 1554 | defs += "\n" + def; 1555 | }; 1556 | 1557 | // note that 1558 | // a) the recursive call to getLink cannot go infinite, because by definition 1559 | // of regex, inner is always a proper substring of wholeMatch, and 1560 | // b) more than one level of nesting is neither supported by the regex 1561 | // nor making a lot of sense (the only use case for nesting is a linked image) 1562 | var getLink = function (wholeMatch, before, inner, afterInner, id, end) { 1563 | inner = inner.replace(regex, getLink); 1564 | if (defsToAdd[id]) { 1565 | addDefNumber(defsToAdd[id]); 1566 | return before + inner + afterInner + refNumber + end; 1567 | } 1568 | return wholeMatch; 1569 | }; 1570 | 1571 | chunk.before = chunk.before.replace(regex, getLink); 1572 | 1573 | if (linkDef) { 1574 | addDefNumber(linkDef); 1575 | } 1576 | else { 1577 | chunk.selection = chunk.selection.replace(regex, getLink); 1578 | } 1579 | 1580 | var refOut = refNumber; 1581 | 1582 | chunk.after = chunk.after.replace(regex, getLink); 1583 | 1584 | if (chunk.after) { 1585 | chunk.after = chunk.after.replace(/\n*$/, ""); 1586 | } 1587 | if (!chunk.after) { 1588 | chunk.selection = chunk.selection.replace(/\n*$/, ""); 1589 | } 1590 | 1591 | chunk.after += "\n\n" + defs; 1592 | 1593 | return refOut; 1594 | }; 1595 | 1596 | // takes the line as entered into the add link/as image dialog and makes 1597 | // sure the URL and the optinal title are "nice". 1598 | function properlyEncoded(linkdef) { 1599 | return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) { 1600 | link = link.replace(/\?.*$/, function (querypart) { 1601 | return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical 1602 | }); 1603 | link = decodeURIComponent(link); // unencode first, to prevent double encoding 1604 | link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29'); 1605 | link = link.replace(/\?.*$/, function (querypart) { 1606 | return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded 1607 | }); 1608 | if (title) { 1609 | title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, ""); 1610 | title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); 1611 | } 1612 | return title ? link + ' "' + title + '"' : link; 1613 | }); 1614 | } 1615 | 1616 | commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) { 1617 | 1618 | chunk.trimWhitespace(); 1619 | chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); 1620 | var background; 1621 | 1622 | if (chunk.endTag.length > 1 && chunk.startTag.length > 0) { 1623 | 1624 | chunk.startTag = chunk.startTag.replace(/!?\[/, ""); 1625 | chunk.endTag = ""; 1626 | this.addLinkDef(chunk, null); 1627 | 1628 | } 1629 | else { 1630 | 1631 | // We're moving start and end tag back into the selection, since (as we're in the else block) we're not 1632 | // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the 1633 | // link text. linkEnteredCallback takes care of escaping any brackets. 1634 | chunk.selection = chunk.startTag + chunk.selection + chunk.endTag; 1635 | chunk.startTag = chunk.endTag = ""; 1636 | 1637 | if (/\n\n/.test(chunk.selection)) { 1638 | this.addLinkDef(chunk, null); 1639 | return; 1640 | } 1641 | var that = this; 1642 | // The function to be executed when you enter a link and press OK or Cancel. 1643 | // Marks up the link and adds the ref. 1644 | var linkEnteredCallback = function (link) { 1645 | 1646 | if (link !== null) { 1647 | // ( $1 1648 | // [^\\] anything that's not a backslash 1649 | // (?:\\\\)* an even number (this includes zero) of backslashes 1650 | // ) 1651 | // (?= followed by 1652 | // [[\]] an opening or closing bracket 1653 | // ) 1654 | // 1655 | // In other words, a non-escaped bracket. These have to be escaped now to make sure they 1656 | // don't count as the end of the link or similar. 1657 | // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets), 1658 | // the bracket in one match may be the "not a backslash" character in the next match, so it 1659 | // should not be consumed by the first match. 1660 | // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the 1661 | // start of the string, so this also works if the selection begins with a bracket. We cannot solve 1662 | // this by anchoring with ^, because in the case that the selection starts with two brackets, this 1663 | // would mean a zero-width match at the start. Since zero-width matches advance the string position, 1664 | // the first bracket could then not act as the "not a backslash" for the second. 1665 | chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1); 1666 | 1667 | var linkDef = " [999]: " + properlyEncoded(link); 1668 | 1669 | var num = that.addLinkDef(chunk, linkDef); 1670 | chunk.startTag = isImage ? "![" : "["; 1671 | chunk.endTag = "][" + num + "]"; 1672 | 1673 | if (!chunk.selection) { 1674 | if (isImage) { 1675 | chunk.selection = "enter image description here"; 1676 | } 1677 | else { 1678 | chunk.selection = "enter link description here"; 1679 | } 1680 | } 1681 | } 1682 | postProcessing(); 1683 | }; 1684 | 1685 | 1686 | if (isImage) { 1687 | if (!this.hooks.insertImageDialog(linkEnteredCallback)) 1688 | ui.prompt('Insert Image', imageDialogText, imageDefaultText, linkEnteredCallback); 1689 | } 1690 | else { 1691 | ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback); 1692 | } 1693 | return true; 1694 | } 1695 | }; 1696 | 1697 | // When making a list, hitting shift-enter will put your cursor on the next line 1698 | // at the current indent level. 1699 | commandProto.doAutoindent = function (chunk, postProcessing) { 1700 | 1701 | var commandMgr = this, 1702 | fakeSelection = false; 1703 | 1704 | chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n"); 1705 | chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n"); 1706 | chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n"); 1707 | 1708 | // There's no selection, end the cursor wasn't at the end of the line: 1709 | // The user wants to split the current list item / code line / blockquote line 1710 | // (for the latter it doesn't really matter) in two. Temporarily select the 1711 | // (rest of the) line to achieve this. 1712 | if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) { 1713 | chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) { 1714 | chunk.selection = wholeMatch; 1715 | return ""; 1716 | }); 1717 | fakeSelection = true; 1718 | } 1719 | 1720 | if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) { 1721 | if (commandMgr.doList) { 1722 | commandMgr.doList(chunk); 1723 | } 1724 | } 1725 | if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) { 1726 | if (commandMgr.doBlockquote) { 1727 | commandMgr.doBlockquote(chunk); 1728 | } 1729 | } 1730 | if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { 1731 | if (commandMgr.doCode) { 1732 | commandMgr.doCode(chunk); 1733 | } 1734 | } 1735 | 1736 | if (fakeSelection) { 1737 | chunk.after = chunk.selection + chunk.after; 1738 | chunk.selection = ""; 1739 | } 1740 | }; 1741 | 1742 | commandProto.doBlockquote = function (chunk, postProcessing) { 1743 | 1744 | chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/, 1745 | function (totalMatch, newlinesBefore, text, newlinesAfter) { 1746 | chunk.before += newlinesBefore; 1747 | chunk.after = newlinesAfter + chunk.after; 1748 | return text; 1749 | }); 1750 | 1751 | chunk.before = chunk.before.replace(/(>[ \t]*)$/, 1752 | function (totalMatch, blankLine) { 1753 | chunk.selection = blankLine + chunk.selection; 1754 | return ""; 1755 | }); 1756 | 1757 | chunk.selection = chunk.selection.replace(/^(\s|>)+$/, ""); 1758 | chunk.selection = chunk.selection || "Blockquote"; 1759 | 1760 | // The original code uses a regular expression to find out how much of the 1761 | // text *directly before* the selection already was a blockquote: 1762 | 1763 | /* 1764 | if (chunk.before) { 1765 | chunk.before = chunk.before.replace(/\n?$/, "\n"); 1766 | } 1767 | chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/, 1768 | function (totalMatch) { 1769 | chunk.startTag = totalMatch; 1770 | return ""; 1771 | }); 1772 | */ 1773 | 1774 | // This comes down to: 1775 | // Go backwards as many lines a possible, such that each line 1776 | // a) starts with ">", or 1777 | // b) is almost empty, except for whitespace, or 1778 | // c) is preceeded by an unbroken chain of non-empty lines 1779 | // leading up to a line that starts with ">" and at least one more character 1780 | // and in addition 1781 | // d) at least one line fulfills a) 1782 | // 1783 | // Since this is essentially a backwards-moving regex, it's susceptible to 1784 | // catstrophic backtracking and can cause the browser to hang; 1785 | // see e.g. http://meta.stackoverflow.com/questions/9807. 1786 | // 1787 | // Hence we replaced this by a simple state machine that just goes through the 1788 | // lines and checks for a), b), and c). 1789 | 1790 | var match = "", 1791 | leftOver = "", 1792 | line; 1793 | if (chunk.before) { 1794 | var lines = chunk.before.replace(/\n$/, "").split("\n"); 1795 | var inChain = false; 1796 | for (var i = 0; i < lines.length; i++) { 1797 | var good = false; 1798 | line = lines[i]; 1799 | inChain = inChain && line.length > 0; // c) any non-empty line continues the chain 1800 | if (/^>/.test(line)) { // a) 1801 | good = true; 1802 | if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain 1803 | inChain = true; 1804 | } else if (/^[ \t]*$/.test(line)) { // b) 1805 | good = true; 1806 | } else { 1807 | good = inChain; // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain 1808 | } 1809 | if (good) { 1810 | match += line + "\n"; 1811 | } else { 1812 | leftOver += match + line; 1813 | match = "\n"; 1814 | } 1815 | } 1816 | if (!/(^|\n)>/.test(match)) { // d) 1817 | leftOver += match; 1818 | match = ""; 1819 | } 1820 | } 1821 | 1822 | chunk.startTag = match; 1823 | chunk.before = leftOver; 1824 | 1825 | // end of change 1826 | 1827 | if (chunk.after) { 1828 | chunk.after = chunk.after.replace(/^\n?/, "\n"); 1829 | } 1830 | 1831 | chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/, 1832 | function (totalMatch) { 1833 | chunk.endTag = totalMatch; 1834 | return ""; 1835 | } 1836 | ); 1837 | 1838 | var replaceBlanksInTags = function (useBracket) { 1839 | 1840 | var replacement = useBracket ? "> " : ""; 1841 | 1842 | if (chunk.startTag) { 1843 | chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/, 1844 | function (totalMatch, markdown) { 1845 | return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; 1846 | }); 1847 | } 1848 | if (chunk.endTag) { 1849 | chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/, 1850 | function (totalMatch, markdown) { 1851 | return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; 1852 | }); 1853 | } 1854 | }; 1855 | 1856 | if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) { 1857 | this.wrap(chunk, SETTINGS.lineLength - 2); 1858 | chunk.selection = chunk.selection.replace(/^/gm, "> "); 1859 | replaceBlanksInTags(true); 1860 | chunk.skipLines(); 1861 | } else { 1862 | chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, ""); 1863 | this.unwrap(chunk); 1864 | replaceBlanksInTags(false); 1865 | 1866 | if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) { 1867 | chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n"); 1868 | } 1869 | 1870 | if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) { 1871 | chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n"); 1872 | } 1873 | } 1874 | 1875 | chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection); 1876 | 1877 | if (!/\n/.test(chunk.selection)) { 1878 | chunk.selection = chunk.selection.replace(/^(> *)/, 1879 | function (wholeMatch, blanks) { 1880 | chunk.startTag += blanks; 1881 | return ""; 1882 | }); 1883 | } 1884 | }; 1885 | 1886 | commandProto.doCode = function (chunk, postProcessing) { 1887 | 1888 | var hasTextBefore = /\S[ ]*$/.test(chunk.before); 1889 | var hasTextAfter = /^[ ]*\S/.test(chunk.after); 1890 | 1891 | // Use 'four space' markdown if the selection is on its own 1892 | // line or is multiline. 1893 | if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) { 1894 | 1895 | chunk.before = chunk.before.replace(/[ ]{4}$/, 1896 | function (totalMatch) { 1897 | chunk.selection = totalMatch + chunk.selection; 1898 | return ""; 1899 | }); 1900 | 1901 | var nLinesBack = 1; 1902 | var nLinesForward = 1; 1903 | 1904 | if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { 1905 | nLinesBack = 0; 1906 | } 1907 | if (/^\n(\t|[ ]{4,})/.test(chunk.after)) { 1908 | nLinesForward = 0; 1909 | } 1910 | 1911 | chunk.skipLines(nLinesBack, nLinesForward); 1912 | 1913 | if (!chunk.selection) { 1914 | chunk.startTag = " "; 1915 | chunk.selection = "enter code here"; 1916 | } 1917 | else { 1918 | if (/^[ ]{0,3}\S/m.test(chunk.selection)) { 1919 | if (/\n/.test(chunk.selection)) 1920 | chunk.selection = chunk.selection.replace(/^/gm, " "); 1921 | else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior 1922 | chunk.before += " "; 1923 | } 1924 | else { 1925 | chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, ""); 1926 | } 1927 | } 1928 | } 1929 | else { 1930 | // Use backticks (`) to delimit the code block. 1931 | 1932 | chunk.trimWhitespace(); 1933 | chunk.findTags(/`/, /`/); 1934 | 1935 | if (!chunk.startTag && !chunk.endTag) { 1936 | chunk.startTag = chunk.endTag = "`"; 1937 | if (!chunk.selection) { 1938 | chunk.selection = "enter code here"; 1939 | } 1940 | } 1941 | else if (chunk.endTag && !chunk.startTag) { 1942 | chunk.before += chunk.endTag; 1943 | chunk.endTag = ""; 1944 | } 1945 | else { 1946 | chunk.startTag = chunk.endTag = ""; 1947 | } 1948 | } 1949 | }; 1950 | 1951 | commandProto.doList = function (chunk, postProcessing, isNumberedList) { 1952 | 1953 | // These are identical except at the very beginning and end. 1954 | // Should probably use the regex extension function to make this clearer. 1955 | var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/; 1956 | var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/; 1957 | 1958 | // The default bullet is a dash but others are possible. 1959 | // This has nothing to do with the particular HTML bullet, 1960 | // it's just a markdown bullet. 1961 | var bullet = "-"; 1962 | 1963 | // The number in a numbered list. 1964 | var num = 1; 1965 | 1966 | // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list. 1967 | var getItemPrefix = function () { 1968 | var prefix; 1969 | if (isNumberedList) { 1970 | prefix = " " + num + ". "; 1971 | num++; 1972 | } 1973 | else { 1974 | prefix = " " + bullet + " "; 1975 | } 1976 | return prefix; 1977 | }; 1978 | 1979 | // Fixes the prefixes of the other list items. 1980 | var getPrefixedItem = function (itemText) { 1981 | 1982 | // The numbering flag is unset when called by autoindent. 1983 | if (isNumberedList === undefined) { 1984 | isNumberedList = /^\s*\d/.test(itemText); 1985 | } 1986 | 1987 | // Renumber/bullet the list element. 1988 | itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm, 1989 | function (_) { 1990 | return getItemPrefix(); 1991 | }); 1992 | 1993 | return itemText; 1994 | }; 1995 | 1996 | chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null); 1997 | 1998 | if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) { 1999 | chunk.before += chunk.startTag; 2000 | chunk.startTag = ""; 2001 | } 2002 | 2003 | if (chunk.startTag) { 2004 | 2005 | var hasDigits = /\d+[.]/.test(chunk.startTag); 2006 | chunk.startTag = ""; 2007 | chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n"); 2008 | this.unwrap(chunk); 2009 | chunk.skipLines(); 2010 | 2011 | if (hasDigits) { 2012 | // Have to renumber the bullet points if this is a numbered list. 2013 | chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem); 2014 | } 2015 | if (isNumberedList == hasDigits) { 2016 | return; 2017 | } 2018 | } 2019 | 2020 | var nLinesUp = 1; 2021 | 2022 | chunk.before = chunk.before.replace(previousItemsRegex, 2023 | function (itemText) { 2024 | if (/^\s*([*+-])/.test(itemText)) { 2025 | bullet = re.$1; 2026 | } 2027 | nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; 2028 | return getPrefixedItem(itemText); 2029 | }); 2030 | 2031 | if (!chunk.selection) { 2032 | chunk.selection = "List item"; 2033 | } 2034 | 2035 | var prefix = getItemPrefix(); 2036 | 2037 | var nLinesDown = 1; 2038 | 2039 | chunk.after = chunk.after.replace(nextItemsRegex, 2040 | function (itemText) { 2041 | nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; 2042 | return getPrefixedItem(itemText); 2043 | }); 2044 | 2045 | chunk.trimWhitespace(true); 2046 | chunk.skipLines(nLinesUp, nLinesDown, true); 2047 | chunk.startTag = prefix; 2048 | var spaces = prefix.replace(/./g, " "); 2049 | this.wrap(chunk, SETTINGS.lineLength - spaces.length); 2050 | chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces); 2051 | 2052 | }; 2053 | 2054 | commandProto.doHeading = function (chunk, postProcessing) { 2055 | 2056 | // Remove leading/trailing whitespace and reduce internal spaces to single spaces. 2057 | chunk.selection = chunk.selection.replace(/\s+/g, " "); 2058 | chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, ""); 2059 | 2060 | // If we clicked the button with no selected text, we just 2061 | // make a level 2 hash header around some default text. 2062 | if (!chunk.selection) { 2063 | chunk.startTag = "## "; 2064 | chunk.selection = "Heading"; 2065 | chunk.endTag = " ##"; 2066 | return; 2067 | } 2068 | 2069 | var headerLevel = 0; // The existing header level of the selected text. 2070 | 2071 | // Remove any existing hash heading markdown and save the header level. 2072 | chunk.findTags(/#+[ ]*/, /[ ]*#+/); 2073 | if (/#+/.test(chunk.startTag)) { 2074 | headerLevel = re.lastMatch.length; 2075 | } 2076 | chunk.startTag = chunk.endTag = ""; 2077 | 2078 | // Try to get the current header level by looking for - and = in the line 2079 | // below the selection. 2080 | chunk.findTags(null, /\s?(-+|=+)/); 2081 | if (/=+/.test(chunk.endTag)) { 2082 | headerLevel = 1; 2083 | } 2084 | if (/-+/.test(chunk.endTag)) { 2085 | headerLevel = 2; 2086 | } 2087 | 2088 | // Skip to the next line so we can create the header markdown. 2089 | chunk.startTag = chunk.endTag = ""; 2090 | chunk.skipLines(1, 1); 2091 | 2092 | // We make a level 2 header if there is no current header. 2093 | // If there is a header level, we substract one from the header level. 2094 | // If it's already a level 1 header, it's removed. 2095 | var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1; 2096 | 2097 | if (headerLevelToCreate > 0) { 2098 | 2099 | // The button only creates level 1 and 2 underline headers. 2100 | // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner? 2101 | var headerChar = headerLevelToCreate >= 2 ? "-" : "="; 2102 | var len = chunk.selection.length; 2103 | if (len > SETTINGS.lineLength) { 2104 | len = SETTINGS.lineLength; 2105 | } 2106 | chunk.endTag = "\n"; 2107 | while (len--) { 2108 | chunk.endTag += headerChar; 2109 | } 2110 | } 2111 | }; 2112 | 2113 | commandProto.doHorizontalRule = function (chunk, postProcessing) { 2114 | chunk.startTag = "----------\n"; 2115 | chunk.selection = ""; 2116 | chunk.skipLines(2, 1, true); 2117 | } 2118 | 2119 | 2120 | })(); 2121 | --------------------------------------------------------------------------------