├── 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 |
|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 = "![]()
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 || Name | 70 |Type | 71 |Default | 72 |Options/Description | 73 |
|---|---|---|---|
| sanitize | 78 |boolean | 79 |true | 80 |true or false, enables or disables sanitization of HTML | 81 |
| help | 84 |function | 85 |null | 86 |A function to be called when the help button it clicked. Null precludes the rendering of the help button. | 87 |
| hooks | 90 |array | 91 |[] | 92 |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 | | 95 |
103 | All of the created elements are wrapped in nice helpful HTML tags as defined: 104 |
105 |'+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='× '+t+"
";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]*(\S+?)>?[ \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 | (\S+?)>? // 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+?)>?(?=\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 | \2> // 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 | .*\2> // 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 | ( // href = $4
515 | (?:
516 | \([^)]*\) // allow one level of (correctly nested) parens (think MSDN)
517 | |
518 | [^()]
519 | )*?
520 | )>?
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 = "" + link_text + "";
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: 
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 | (\S+?)>? // 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]*()(\S+?)>?[ \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(/";
707 |
708 | return result;
709 | }
710 |
711 | function _DoHeaders(text) {
712 |
713 | // Setext-style headers:
714 | // Header 1
715 | // ========
716 | //
717 | // Header 2
718 | // --------
719 | //
720 | text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
721 | function (wholeMatch, m1) { return "" + _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 `$list_type>`
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 + "" + list_type + ">\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 + "" + list_type + ">\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 | //
1025 | //
1026 | // ×
1027 | // Modal header
1028 | //
1029 | //
1030 | // One fine body…
1031 | //
1032 | //
1036 | //
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 = '× '+title+'
';
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]*(\S+?)>?[ \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 |
--------------------------------------------------------------------------------