├── .gitignore ├── README.md ├── css ├── editor.css └── facebox.css ├── editor.html ├── images ├── closelabel.png ├── icon-sprite.png └── loading.gif ├── index.html └── javascript ├── application.js ├── editor.js ├── facebox.js ├── jquery.js └── showdown.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Caution: 2 | This is a work in progress! 3 | 4 | Click [here](http://jfrolich.github.com/simple-markdown-editor/) to see it working -------------------------------------------------------------------------------- /css/editor.css: -------------------------------------------------------------------------------- 1 | /* 2 | editor.css 3 | Wiki editor formatting 4 | */ 5 | body, html { 6 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 7 | font-size: 10px; /* -> 1em */ 8 | margin: 0; 9 | padding: 0; 10 | } 11 | fieldset { 12 | border: 0; 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | 18 | #facebox .preview { 19 | font-size: 1.3em; 20 | } 21 | 22 | .live-preview { 23 | font-size: 1.3em; 24 | clear: both; 25 | } 26 | 27 | #markdown-editor-dialog { 28 | padding: 10px; 29 | } 30 | 31 | #markdown-editor-dialog h4 { 32 | border-bottom: 1px solid #DDD; 33 | color: black; 34 | font-size: 1.8em; 35 | font-weight: bold; 36 | line-height: normal; 37 | margin: 0px 0px 0.75em; 38 | padding: 0px 0px 0.3em; 39 | } 40 | 41 | #markdown-editor-dialog a.minibutton { 42 | float: left; 43 | } 44 | #markdown-editor-dialog input { 45 | border: 1px solid #DDD; 46 | display: block; 47 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 48 | font-size: 1.2em; 49 | line-height: 1.6em; 50 | margin: 0.3em 0px 0px; 51 | padding: 0.3em 0.5em; 52 | width: 300px; 53 | } 54 | 55 | #markdown-editor-dialog label { 56 | color: black; 57 | display: block; 58 | font-size: 1.2em; 59 | font-weight: bold; 60 | line-height: 1.6em; 61 | margin: 0px; 62 | min-width: 80px; 63 | padding: 0px; 64 | } 65 | 66 | #markdown-editor-dialog a.minibutton { 67 | margin-right: 10px; 68 | 69 | } 70 | 71 | #markdown-editor-dialog .buttons { 72 | margin-top: 10px; 73 | 74 | } 75 | 76 | 77 | a { 78 | -moz-outline: none !important; 79 | } 80 | 81 | .editor { 82 | border: 1px solid #e4e4e4; 83 | background: #f9f9f9; 84 | margin: 1em 0 5em; 85 | overflow: hidden; 86 | padding: 1em 1em 0.4em; 87 | } 88 | 89 | .ie .editor { 90 | padding-bottom: 1em; 91 | } 92 | 93 | /* @control function-bar */ 94 | 95 | 96 | .editor .function-bar .function-buttons { 97 | display: block; 98 | float: left; 99 | overflow: hidden; 100 | padding: 0 0 5px 0; 101 | } 102 | 103 | .editor .function-bar a.function-button { 104 | background: #f7f7f7; 105 | border: 1px solid #ddd; 106 | color: #333; 107 | display: block; 108 | float: left; 109 | height: 25px; 110 | overflow: hidden; 111 | margin: 0.2em 0.5em 0 0; 112 | /* text-indent: -5000px; */ 113 | text-shadow: 0 1px 0 #fff; 114 | width: 25px; 115 | 116 | border-radius: 0.3em; 117 | -moz-border-radius: 0.3em; 118 | -webkit-border-radius: 0.3em; 119 | 120 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f4f4f4', endColorstr='#ececec'); 121 | background: -webkit-gradient(linear, left top, left bottom, from(#f4f4f4), to(#ececec)); 122 | background: -moz-linear-gradient(top, #f4f4f4, #ececec); 123 | } 124 | 125 | .editor .function-bar a.function-button:hover { 126 | color: #fff; 127 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); 128 | text-decoration: none; 129 | 130 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#599bdc', endColorstr='#3072b3'); 131 | background: -webkit-gradient(linear, left top, left bottom, from(#599bdc), to(#3072b3)); 132 | background: -moz-linear-gradient(top, #599bdc, #3072b3); 133 | border: 1px solid; 134 | border-color: #518cc6 #518cc6 #2a65a0; 135 | } 136 | 137 | .editor .function-bar a span { 138 | background-image: url(../images/icon-sprite.png); 139 | background-repeat: no-repeat; 140 | display: block; 141 | height: 25px; 142 | overflow: hidden; 143 | text-indent: -5000px; 144 | width: 25px; 145 | } 146 | 147 | a.function-bold span { background-position: 0 0; } 148 | a.function-italic span { background-position: -27px 0; } 149 | a.function-underline span { background-position: -54px 0; } 150 | a.function-code span { background-position: -82px 0; } 151 | a.function-ul span { background-position: -109px 0; } 152 | a.function-ol span { background-position: -136px 0; } 153 | a.function-blockquote span { background-position: -163px 0; } 154 | a.function-hr span { background-position: -190px 0; } 155 | a.function-h1 span { background-position: -217px 0; } 156 | a.function-h2 span { background-position: -244px 0; } 157 | a.function-h3 span { background-position: -271px 0; } 158 | a.function-link span { background-position: -298px 0; } 159 | a.function-image span { background-position: -324px 0; } 160 | a.function-help span { background-position: -405px 0; } 161 | 162 | a.function-bold:hover span { background-position: 0 -28px; } 163 | a.function-italic:hover span { background-position: -27px -28px; } 164 | a.function-underline:hover span { background-position: -54px -28px; } 165 | a.function-code:hover span { background-position: -82px -28px; } 166 | a.function-ul:hover span { background-position: -109px -28px; } 167 | a.function-ol:hover span { background-position: -136px -28px; } 168 | a.function-blockquote:hover span { background-position: -163px -28px; } 169 | a.function-hr:hover span { background-position: -190px -28px; } 170 | a.function-h1:hover span { background-position: -217px -28px; } 171 | a.function-h2:hover span { background-position: -244px -28px; } 172 | a.function-h3:hover span { background-position: -271px -28px; } 173 | a.function-link:hover span { background-position: -298px -28px; } 174 | a.function-image:hover span { background-position: -324px -28px; } 175 | a.function-help:hover span { background-position: -405px -28px; } 176 | 177 | 178 | .editor .function-bar a.disabled { 179 | display: none; 180 | } 181 | 182 | .editor .function-bar span.function-divider { 183 | display: block; 184 | float: left; 185 | width: 0.5em; 186 | } 187 | 188 | .editor .collapsed a span, 189 | .editor .expanded a span { 190 | background-image: url(../images/icon-sprite.png); 191 | background-position: -351px -1px; 192 | background-repeat: no-repeat; 193 | display: block; 194 | height: 25px; 195 | overflow: hidden; 196 | text-indent: -5000px; 197 | width: 25px; 198 | } 199 | 200 | 201 | .editor textarea { 202 | background-color: #fff; 203 | border: 1px solid #ddd; 204 | clear: both; 205 | display: block; 206 | font-size: 1.3em; 207 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 208 | height: 7em; 209 | line-height: 1.8em; 210 | margin: 0.7em 0; 211 | padding: 0.5em; 212 | width: 98%; 213 | } 214 | 215 | a.minibutton, 216 | a.minibutton:visited { 217 | background-color: #f7f7f7; 218 | border: 1px solid #d4d4d4; 219 | color: #333; 220 | cursor: pointer; 221 | display: block; 222 | font-size: 1.2em; 223 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 224 | font-weight: bold; 225 | line-height: 1.2em; 226 | margin: 0 0 0 0em; 227 | padding: 0.5em 1em; 228 | text-decoration: none; 229 | 230 | text-shadow: 0 1px 0 #fff; 231 | 232 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f4f4f4', endColorstr='#ececec'); 233 | background: -webkit-gradient(linear, left top, left bottom, from(#f4f4f4), to(#ececec)); 234 | background: -moz-linear-gradient(top, #f4f4f4, #ececec); 235 | 236 | border-radius: 3px; 237 | -moz-border-radius: 3px; 238 | -webkit-border-radius: 3px; 239 | } 240 | 241 | a.minibutton:hover { 242 | background: #3072b3; 243 | border-color: #518cc6 #518cc6 #2a65a0; 244 | color: #fff; 245 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); 246 | text-decoration: none; 247 | 248 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#599bdc', endColorstr='#3072b3'); 249 | background: -webkit-gradient(linear, left top, left bottom, from(#599bdc), to(#3072b3)); 250 | background: -moz-linear-gradient(top, #599bdc, #3072b3); 251 | } 252 | .preview { 253 | float: left; 254 | font-weight: normal; 255 | padding: left; 256 | } 257 | 258 | input.submit { 259 | background-color: #f7f7f7; 260 | border: 1px solid #d4d4d4; 261 | color: #333; 262 | cursor: pointer; 263 | display: block; 264 | float: left; 265 | font-size: 1.2em; 266 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 267 | font-weight: bold; 268 | margin: 0; 269 | padding: 0.4em 1em; 270 | 271 | text-shadow: 0 1px 0 #fff; 272 | 273 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#f4f4f4', endColorstr='#ececec'); 274 | background: -webkit-gradient(linear, left top, left bottom, from(#f4f4f4), to(#ececec)); 275 | background: -moz-linear-gradient(top, #f4f4f4, #ececec); 276 | 277 | border-radius: 3px; 278 | -moz-border-radius: 3px; 279 | -webkit-border-radius: 3px; 280 | } 281 | 282 | .webkit input.submit { 283 | padding: 0.5em 1em 0.45em; 284 | } 285 | 286 | .ie input.submit { 287 | padding: 0.4em 1em 0.5em; 288 | } 289 | 290 | input.submit:hover { 291 | background: #3072b3; 292 | border-color: #518cc6 #518cc6 #2a65a0; 293 | color: #fff; 294 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); 295 | text-decoration: none; 296 | 297 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0, startColorstr='#599bdc', endColorstr='#3072b3'); 298 | background: -webkit-gradient(linear, left top, left bottom, from(#599bdc), to(#3072b3)); 299 | background: -moz-linear-gradient(top, #599bdc, #3072b3); 300 | } 301 | 302 | .editor-preview { 303 | float: left; 304 | font-weight: normal; 305 | padding: left; 306 | } -------------------------------------------------------------------------------- /css/facebox.css: -------------------------------------------------------------------------------- 1 | #facebox { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | z-index: 100; 6 | text-align: left; 7 | } 8 | 9 | 10 | #facebox .popup{ 11 | position:relative; 12 | border:3px solid rgba(0,0,0,0); 13 | -webkit-border-radius:5px; 14 | -moz-border-radius:5px; 15 | border-radius:5px; 16 | -webkit-box-shadow:0 0 18px rgba(0,0,0,0.4); 17 | -moz-box-shadow:0 0 18px rgba(0,0,0,0.4); 18 | box-shadow:0 0 18px rgba(0,0,0,0.4); 19 | } 20 | 21 | #facebox .content { 22 | display:table; 23 | min-width: 350px; 24 | padding: 10px; 25 | background: #fff; 26 | -webkit-border-radius:4px; 27 | -moz-border-radius:4px; 28 | border-radius:4px; 29 | } 30 | 31 | #facebox .content > p:first-child{ 32 | margin-top:0; 33 | } 34 | #facebox .content > p:last-child{ 35 | margin-bottom:0; 36 | } 37 | 38 | #facebox .close{ 39 | position:absolute; 40 | top:5px; 41 | right:5px; 42 | padding:2px; 43 | background:#fff; 44 | } 45 | #facebox .close img{ 46 | opacity:0.3; 47 | } 48 | #facebox .close:hover img{ 49 | opacity:1.0; 50 | } 51 | 52 | #facebox .loading { 53 | text-align: center; 54 | } 55 | 56 | #facebox .image { 57 | text-align: center; 58 | } 59 | 60 | #facebox img { 61 | border: 0; 62 | margin: 0; 63 | } 64 | 65 | #facebox_overlay { 66 | position: fixed; 67 | top: 0px; 68 | left: 0px; 69 | min-height:100%; 70 | width:100%; 71 | } 72 | 73 | .facebox_hide { 74 | z-index:-100; 75 | } 76 | 77 | .facebox_overlayBG { 78 | background-color: #000; 79 | z-index: 99; 80 | } -------------------------------------------------------------------------------- /editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Editor 13 | 14 | 15 |
16 |
17 | 18 |
19 | Preview 20 |
21 |
22 | 23 |
24 | Preview 25 |
26 | 27 |
28 | 29 | -------------------------------------------------------------------------------- /images/closelabel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfrolich/simple-markdown-editor/eb9b6a47a807e766eae8822418b9de71c641ba05/images/closelabel.png -------------------------------------------------------------------------------- /images/icon-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfrolich/simple-markdown-editor/eb9b6a47a807e766eae8822418b9de71c641ba05/images/icon-sprite.png -------------------------------------------------------------------------------- /images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfrolich/simple-markdown-editor/eb9b6a47a807e766eae8822418b9de71c641ba05/images/loading.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Editor 13 | 14 | 15 |
16 |
17 | 18 |
19 | Preview 20 |
21 |
22 | 23 |
24 | Preview 25 |
26 | 27 |
28 | 29 | -------------------------------------------------------------------------------- /javascript/application.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $("textarea").markdownEditor() 3 | }) 4 | -------------------------------------------------------------------------------- /javascript/editor.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | 3 | // Private functions 4 | getSelectionPosition = function($field ) { 5 | if ($field.length) { 6 | var start = 0, end = 0 7 | var el = $field.get(0) 8 | 9 | if (typeof el.selectionStart == "number" && 10 | typeof el.selectionEnd == "number") { 11 | start = el.selectionStart 12 | end = el.selectionEnd 13 | } 14 | else { 15 | var range = document.selection.createRange() 16 | var stored_range = range.duplicate() 17 | stored_range.moveToElementText( el ) 18 | stored_range.setEndPoint( 'EndToEnd', range ) 19 | start = stored_range.text.length - range.text.length 20 | end = start + range.text.length 21 | 22 | // so, uh, we're close, but we need to search for line breaks and 23 | // adjust the start/end points accordingly since IE counts them as 24 | // 2 characters in TextRange. 25 | var s = start 26 | var lb = 0 27 | var i 28 | 29 | for ( i=0; i < s; i++ ) 30 | if (el.value.charAt(i).match(/\r/)) ++lb 31 | 32 | if (lb) { 33 | start = start - lb 34 | lb = 0 35 | } 36 | 37 | var e = end 38 | for (i=0; i < e; i++) 39 | if (el.value.charAt(i).match(/\r/) ) ++lb 40 | if (lb) end = end - lb 41 | } 42 | return { 43 | start: start, 44 | end: end 45 | } 46 | } 47 | } 48 | 49 | getSelection = function( $field ) { 50 | var selStr = '' 51 | var selPos 52 | 53 | if ($field.length) { 54 | selPos = getSelectionPosition($field) 55 | selStr = $field.val().substring(selPos.start, selPos.end) 56 | return selStr 57 | } 58 | return false 59 | } 60 | 61 | replaceSelection = function($field, replaceText, reselect, cursorOffset) { 62 | var selPos = getSelectionPosition($field) 63 | var fullStr = $field.val() 64 | var selectNew = true 65 | if (reselect === false) selectNew = false 66 | 67 | var scrollTop = null 68 | if ($field[0].scrollTop) scrollTop = $field[0].scrollTop 69 | 70 | $field.val(fullStr.substring(0, selPos.start) + replaceText + fullStr.substring(selPos.end)) 71 | $field[0].focus() 72 | 73 | if (selectNew) { 74 | if ($field[0].setSelectionRange) { 75 | if (cursorOffset) { 76 | $field[0].setSelectionRange(selPos.start + cursorOffset, selPos.start + cursorOffset) 77 | } else { 78 | $field[0].setSelectionRange(selPos.start, selPos.start + replaceText.length) 79 | } 80 | } else if ($field[0].createTextRange) { 81 | var range = $field[0].createTextRange() 82 | range.collapse( true ) 83 | if (cursorOffset) { 84 | range.moveEnd(selPos.start + cursorOffset) 85 | range.moveStart(selPos.start + cursorOffset) 86 | } else { 87 | range.moveEnd('character', selPos.start + replaceText.length) 88 | range.moveStart('character', selPos.start) 89 | } 90 | range.select() 91 | } 92 | } 93 | 94 | if (scrollTop) { 95 | // this jumps sometimes in FF 96 | $field[0].scrollTop = scrollTop 97 | } 98 | } 99 | 100 | selectWholeLine = function($field) { 101 | var selPos = getSelectionPosition( $field ) 102 | var el = $field.get(0) 103 | var i 104 | for ( i=selPos.start-1; i >= 0; i-- ) 105 | if ( el.value.charAt(i).match(/\n/) ) break 106 | i++ 107 | $field[0].setSelectionRange(i, selPos.end) 108 | } 109 | 110 | // Public function 111 | $.markdownEditor = {} 112 | $.markdownEditor.executeAction = function($editor, search, replace, whole_line, append) { 113 | var textElement = $editor.find('textarea') 114 | if (whole_line) selectWholeLine(textElement) 115 | var txt = textElement.val() 116 | var selPos = getSelectionPosition(textElement) 117 | var selText = getSelection(textElement) 118 | var repText = selText 119 | var reselect = true 120 | var cursor = null 121 | 122 | if (search && replace) { 123 | repText = repText.replace(search, replace) 124 | // remove backreferences 125 | repText = repText.replace(/\$[\d]/g, '') 126 | if ( repText === '' ) { 127 | cursor = replace.indexOf('$1') 128 | 129 | repText = replace.replace( /\$[\d]/g, '' ) 130 | if ( cursor == -1 ) cursor = Math.floor( replace.length / 2 ) 131 | } 132 | } 133 | 134 | if (append) { 135 | if ( repText == selText ) reselect = false 136 | repText += append 137 | } 138 | if (repText) replaceSelection( textElement, repText, reselect, cursor) 139 | $editor.find(".editor-body").keyup() 140 | } 141 | 142 | addFunctionBar = function($field) { 143 | $field.before('\ 144 |
\ 145 |
\ 146 | \ 147 | Bold\ 148 | \ 149 | Italic\ 150 | \ 151 | Code\ 152 |  \ 153 | \ 154 | Unordered List\ 155 | \ 156 | Ordered List\ 157 | \ 158 | Blockquote\ 159 | \ 160 | Horizontal Rule\ 161 |  \ 162 | \ 163 | h1\ 164 | \ 165 | h2\ 166 | \ 167 | h3\ 168 |  \ 169 | \ 170 | Link\ 171 | \ 172 | Image\ 173 |  \ 174 | \ 175 | Help\ 176 |
\ 177 |
') 178 | } 179 | 180 | makeBindings = function($editor) { 181 | var converter = new Showdown.converter; 182 | $editor 183 | .delegate(".function-button", "click", function(e){ 184 | e.preventDefault 185 | }) 186 | .delegate(".function-bold","click", function() { 187 | $.markdownEditor.executeAction($editor, /([^\n]+)([\n\s]*)/g, "**$1**$2") 188 | }) 189 | .delegate(".function-italic","click", function() { 190 | $.markdownEditor.executeAction($editor, /([^\n]+)([\n\s]*)/g, "_$1_$2") 191 | }) 192 | .delegate(".function-code","click", function() { 193 | $.markdownEditor.executeAction($editor, /([\s\S]*)/, "`$1`") 194 | }) 195 | .delegate(".function-hr","click", function() { 196 | $.markdownEditor.executeAction($editor, false,false, false,"\n***\n") 197 | }) 198 | .delegate(".function-ul","click", function() { 199 | $.markdownEditor.executeAction($editor, /(.+)([\n]?)/g, "* $1$2", true) 200 | }) 201 | .delegate(".function-ol","click", function() { 202 | var n = 0 203 | $.markdownEditor.executeAction($editor, /(.+)([\n]?)/g, function(str, p1, p2, offset, s) { return ++n + ". " + p1 + p2}, true) 204 | }) 205 | .delegate(".function-blockquote","click", function() { 206 | $.markdownEditor.executeAction($editor, /(.+)([\n]?)/g, "> $1$2") 207 | }) 208 | .delegate(".function-h1","click", function() { 209 | $.markdownEditor.executeAction($editor, /(.+)([\n]?)/g, "# $1$2", true) 210 | }) 211 | .delegate(".function-h2","click", function() { 212 | $.markdownEditor.executeAction($editor, /(.+)([\n]?)/g, "## $1$2", true) 213 | }) 214 | .delegate(".function-h3","click", function() { 215 | $.markdownEditor.executeAction($editor, /(.+)([\n]?)/g, "### $1$2", true) 216 | }) 217 | .delegate(".function-link", "click", function() { 218 | console.log($editor) 219 | $.facebox('

Insert Link

CancelOK
') 220 | $("#markdown-editor-dialog").data("editor", $editor) 221 | }) 222 | 223 | $("body").delegate(".ok", "click", function() { 224 | dialog = $(this).closest("#markdown-editor-dialog") 225 | href = dialog.find(".href").val() 226 | text = dialog.find(".text").val() 227 | if (href && text) 228 | $.markdownEditor.executeAction(dialog.data("editor"), /([\s\S]*)/, '[' + text + '](' + href + ')') 229 | $.facebox.close() 230 | }) 231 | 232 | $("body").delegate(".cancel", "click", function() { 233 | $.facebox.close() 234 | }) 235 | 236 | $editor.find('.preview').bind("click", function() { 237 | $.facebox('
' + converter.makeHtml($editor.find('.editor-body').val()) + '
') 238 | }) 239 | $editor.find('.editor-body').keyup(function() { 240 | var txt = $editor.find('.editor-body').val() 241 | var html = converter.makeHtml(txt) 242 | $editor.find(".live-preview").html(html) 243 | }) 244 | } 245 | 246 | // Jquery plugin 247 | var methods = { 248 | init: function(options) { 249 | result = this.each(function() { 250 | if ( options ) { 251 | $.extend( settings, options ) 252 | } 253 | $(this).addClass('editor-body') 254 | if ($(this).closest('fieldset').length) $(this).closest('fieldset').addClass('editor') 255 | else $(this).closest('form').addClass('editor') 256 | addFunctionBar($(this)) 257 | makeBindings($(this).closest(".editor")) 258 | }) 259 | return result 260 | }, 261 | addFunctionBar: function() { 262 | this.each(function() { 263 | addFunctionBar($(this)) 264 | }) 265 | } 266 | } 267 | 268 | 269 | $.fn.markdownEditor = function(method) { 270 | var settings = { 271 | 'function-bar' : 'basic' 272 | } 273 | if (methods[method]) return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)) 274 | 275 | else if (typeof method === 'object' || ! method) 276 | return methods.init.apply( this, arguments ) 277 | else 278 | $.error('Method ' + method + ' does not exist on jQuery.markdownEditor' ) 279 | } 280 | 281 | })(jQuery) -------------------------------------------------------------------------------- /javascript/facebox.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Facebox (for jQuery) 3 | * version: 1.3 4 | * @requires jQuery v1.2 or later 5 | * @homepage https://github.com/defunkt/facebox 6 | * 7 | * Licensed under the MIT: 8 | * http://www.opensource.org/licenses/mit-license.php 9 | * 10 | * Copyright Forever Chris Wanstrath, Kyle Neath 11 | * 12 | * Usage: 13 | * 14 | * jQuery(document).ready(function() { 15 | * jQuery('a[rel*=facebox]').facebox() 16 | * }) 17 | * 18 | * Terms 19 | * Loads the #terms div in the box 20 | * 21 | * Terms 22 | * Loads the terms.html page in the box 23 | * 24 | * Terms 25 | * Loads the terms.png image in the box 26 | * 27 | * 28 | * You can also use it programmatically: 29 | * 30 | * jQuery.facebox('some html') 31 | * jQuery.facebox('some html', 'my-groovy-style') 32 | * 33 | * The above will open a facebox with "some html" as the content. 34 | * 35 | * jQuery.facebox(function($) { 36 | * $.get('blah.html', function(data) { $.facebox(data) }) 37 | * }) 38 | * 39 | * The above will show a loading screen before the passed function is called, 40 | * allowing for a better ajaxy experience. 41 | * 42 | * The facebox function can also display an ajax page, an image, or the contents of a div: 43 | * 44 | * jQuery.facebox({ ajax: 'remote.html' }) 45 | * jQuery.facebox({ ajax: 'remote.html' }, 'my-groovy-style') 46 | * jQuery.facebox({ image: 'stairs.jpg' }) 47 | * jQuery.facebox({ image: 'stairs.jpg' }, 'my-groovy-style') 48 | * jQuery.facebox({ div: '#box' }) 49 | * jQuery.facebox({ div: '#box' }, 'my-groovy-style') 50 | * 51 | * Want to close the facebox? Trigger the 'close.facebox' document event: 52 | * 53 | * jQuery(document).trigger('close.facebox') 54 | * 55 | * Facebox also has a bunch of other hooks: 56 | * 57 | * loading.facebox 58 | * beforeReveal.facebox 59 | * reveal.facebox (aliased as 'afterReveal.facebox') 60 | * init.facebox 61 | * afterClose.facebox 62 | * 63 | * Simply bind a function to any of these hooks: 64 | * 65 | * $(document).bind('reveal.facebox', function() { ...stuff to do after the facebox and contents are revealed... }) 66 | * 67 | */ 68 | (function($) { 69 | $.facebox = function(data, klass) { 70 | $.facebox.loading() 71 | 72 | if (data.ajax) fillFaceboxFromAjax(data.ajax, klass) 73 | else if (data.image) fillFaceboxFromImage(data.image, klass) 74 | else if (data.div) fillFaceboxFromHref(data.div, klass) 75 | else if ($.isFunction(data)) data.call($) 76 | else $.facebox.reveal(data, klass) 77 | } 78 | 79 | /* 80 | * Public, $.facebox methods 81 | */ 82 | 83 | $.extend($.facebox, { 84 | settings: { 85 | opacity : 0.2, 86 | overlay : true, 87 | loadingImage : 'images/loading.gif', 88 | closeImage : 'images/closelabel.png', 89 | imageTypes : [ 'png', 'jpg', 'jpeg', 'gif' ], 90 | faceboxHtml : '\ 91 | ' 98 | }, 99 | 100 | loading: function() { 101 | init() 102 | if ($('#facebox .loading').length == 1) return true 103 | showOverlay() 104 | 105 | $('#facebox .content').empty(). 106 | append('
') 107 | 108 | $('#facebox').show().css({ 109 | top: getPageScroll()[1] + (getPageHeight() / 10), 110 | left: $(window).width() / 2 - ($('#facebox .popup').outerWidth() / 2) 111 | }) 112 | 113 | $(document).bind('keydown.facebox', function(e) { 114 | if (e.keyCode == 27) $.facebox.close() 115 | return true 116 | }) 117 | $(document).trigger('loading.facebox') 118 | }, 119 | 120 | reveal: function(data, klass) { 121 | $(document).trigger('beforeReveal.facebox') 122 | if (klass) $('#facebox .content').addClass(klass) 123 | $('#facebox .content').empty().append(data) 124 | $('#facebox .popup').children().fadeIn('normal') 125 | $('#facebox').css('left', $(window).width() / 2 - ($('#facebox .popup').outerWidth() / 2)) 126 | $(document).trigger('reveal.facebox').trigger('afterReveal.facebox') 127 | }, 128 | 129 | close: function() { 130 | $(document).trigger('close.facebox') 131 | return false 132 | } 133 | }) 134 | 135 | /* 136 | * Public, $.fn methods 137 | */ 138 | 139 | $.fn.facebox = function(settings) { 140 | if ($(this).length == 0) return 141 | 142 | init(settings) 143 | 144 | function clickHandler() { 145 | $.facebox.loading(true) 146 | 147 | // support for rel="facebox.inline_popup" syntax, to add a class 148 | // also supports deprecated "facebox[.inline_popup]" syntax 149 | var klass = this.rel.match(/facebox\[?\.(\w+)\]?/) 150 | if (klass) klass = klass[1] 151 | 152 | fillFaceboxFromHref(this.href, klass) 153 | return false 154 | } 155 | 156 | return this.bind('click.facebox', clickHandler) 157 | } 158 | 159 | /* 160 | * Private methods 161 | */ 162 | 163 | // called one time to setup facebox on this page 164 | function init(settings) { 165 | if ($.facebox.settings.inited) return true 166 | else $.facebox.settings.inited = true 167 | 168 | $(document).trigger('init.facebox') 169 | makeCompatible() 170 | 171 | var imageTypes = $.facebox.settings.imageTypes.join('|') 172 | $.facebox.settings.imageTypesRegexp = new RegExp('\\.(' + imageTypes + ')(\\?.*)?$', 'i') 173 | 174 | if (settings) $.extend($.facebox.settings, settings) 175 | $('body').append($.facebox.settings.faceboxHtml) 176 | 177 | var preload = [ new Image(), new Image() ] 178 | preload[0].src = $.facebox.settings.closeImage 179 | preload[1].src = $.facebox.settings.loadingImage 180 | 181 | $('#facebox').find('.b:first, .bl').each(function() { 182 | preload.push(new Image()) 183 | preload.slice(-1).src = $(this).css('background-image').replace(/url\((.+)\)/, '$1') 184 | }) 185 | 186 | $('#facebox .close') 187 | .click($.facebox.close) 188 | .append('') 191 | } 192 | 193 | // getPageScroll() by quirksmode.com 194 | function getPageScroll() { 195 | var xScroll, yScroll; 196 | if (self.pageYOffset) { 197 | yScroll = self.pageYOffset; 198 | xScroll = self.pageXOffset; 199 | } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict 200 | yScroll = document.documentElement.scrollTop; 201 | xScroll = document.documentElement.scrollLeft; 202 | } else if (document.body) {// all other Explorers 203 | yScroll = document.body.scrollTop; 204 | xScroll = document.body.scrollLeft; 205 | } 206 | return new Array(xScroll,yScroll) 207 | } 208 | 209 | // Adapted from getPageSize() by quirksmode.com 210 | function getPageHeight() { 211 | var windowHeight 212 | if (self.innerHeight) { // all except Explorer 213 | windowHeight = self.innerHeight; 214 | } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode 215 | windowHeight = document.documentElement.clientHeight; 216 | } else if (document.body) { // other Explorers 217 | windowHeight = document.body.clientHeight; 218 | } 219 | return windowHeight 220 | } 221 | 222 | // Backwards compatibility 223 | function makeCompatible() { 224 | var $s = $.facebox.settings 225 | 226 | $s.loadingImage = $s.loading_image || $s.loadingImage 227 | $s.closeImage = $s.close_image || $s.closeImage 228 | $s.imageTypes = $s.image_types || $s.imageTypes 229 | $s.faceboxHtml = $s.facebox_html || $s.faceboxHtml 230 | } 231 | 232 | // Figures out what you want to display and displays it 233 | // formats are: 234 | // div: #id 235 | // image: blah.extension 236 | // ajax: anything else 237 | function fillFaceboxFromHref(href, klass) { 238 | // div 239 | if (href.match(/#/)) { 240 | var url = window.location.href.split('#')[0] 241 | var target = href.replace(url,'') 242 | if (target == '#') return 243 | $.facebox.reveal($(target).html(), klass) 244 | 245 | // image 246 | } else if (href.match($.facebox.settings.imageTypesRegexp)) { 247 | fillFaceboxFromImage(href, klass) 248 | // ajax 249 | } else { 250 | fillFaceboxFromAjax(href, klass) 251 | } 252 | } 253 | 254 | function fillFaceboxFromImage(href, klass) { 255 | var image = new Image() 256 | image.onload = function() { 257 | $.facebox.reveal('
', klass) 258 | } 259 | image.src = href 260 | } 261 | 262 | function fillFaceboxFromAjax(href, klass) { 263 | $.get(href, function(data) { $.facebox.reveal(data, klass) }) 264 | } 265 | 266 | function skipOverlay() { 267 | return $.facebox.settings.overlay == false || $.facebox.settings.opacity === null 268 | } 269 | 270 | function showOverlay() { 271 | if (skipOverlay()) return 272 | 273 | if ($('#facebox_overlay').length == 0) 274 | $("body").append('
') 275 | 276 | $('#facebox_overlay').hide().addClass("facebox_overlayBG") 277 | .css('opacity', $.facebox.settings.opacity) 278 | .click(function() { $(document).trigger('close.facebox') }) 279 | .fadeIn(200) 280 | return false 281 | } 282 | 283 | function hideOverlay() { 284 | if (skipOverlay()) return 285 | 286 | $('#facebox_overlay').fadeOut(200, function(){ 287 | $("#facebox_overlay").removeClass("facebox_overlayBG") 288 | $("#facebox_overlay").addClass("facebox_hide") 289 | $("#facebox_overlay").remove() 290 | }) 291 | 292 | return false 293 | } 294 | 295 | /* 296 | * Bindings 297 | */ 298 | 299 | $(document).bind('close.facebox', function() { 300 | $(document).unbind('keydown.facebox') 301 | $('#facebox').fadeOut(function() { 302 | $('#facebox .content').removeClass().addClass('content') 303 | $('#facebox .loading').remove() 304 | $(document).trigger('afterClose.facebox') 305 | }) 306 | hideOverlay() 307 | }) 308 | 309 | })(jQuery); 310 | -------------------------------------------------------------------------------- /javascript/showdown.js: -------------------------------------------------------------------------------- 1 | // 2 | // showdown.js -- A javascript port of Markdown. 3 | // 4 | // Copyright (c) 2007 John Fraser. 5 | // 6 | // Original Markdown Copyright (c) 2004-2005 John Gruber 7 | // 8 | // 9 | // Redistributable under a BSD-style open source license. 10 | // See license.txt for more information. 11 | // 12 | // The full source distribution is at: 13 | // 14 | // A A L 15 | // T C A 16 | // T K B 17 | // 18 | // 19 | // 20 | 21 | // 22 | // Wherever possible, Showdown is a straight, line-by-line port 23 | // of the Perl version of Markdown. 24 | // 25 | // This is not a normal parser design; it's basically just a 26 | // series of string substitutions. It's hard to read and 27 | // maintain this way, but keeping Showdown close to the original 28 | // design makes it easier to port new features. 29 | // 30 | // More importantly, Showdown behaves like markdown.pl in most 31 | // edge cases. So web applications can do client-side preview 32 | // in Javascript, and then build identical HTML on the server. 33 | // 34 | // This port needs the new RegExp functionality of ECMA 262, 35 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 36 | // should do fine. Even with the new regular expression features, 37 | // We do a lot of work to emulate Perl's regex functionality. 38 | // The tricky changes in this file mostly have the "attacklab:" 39 | // label. Major or self-explanatory changes don't. 40 | // 41 | // Smart diff tools like Araxis Merge will be able to match up 42 | // this file with markdown.pl in a useful way. A little tweaking 43 | // helps: in a copy of markdown.pl, replace "#" with "//" and 44 | // replace "$text" with "text". Be sure to ignore whitespace 45 | // and line endings. 46 | // 47 | 48 | 49 | // 50 | // Showdown usage: 51 | // 52 | // var text = "Markdown *rocks*."; 53 | // 54 | // var converter = new Showdown.converter(); 55 | // var html = converter.makeHtml(text); 56 | // 57 | // alert(html); 58 | // 59 | // Note: move the sample code to the bottom of this 60 | // file before uncommenting it. 61 | // 62 | 63 | 64 | // ************************************************** 65 | // GitHub Flavored Markdown modifications by Tekkub 66 | // http://github.github.com/github-flavored-markdown/ 67 | // 68 | // Modifications are tagged with "GFM" 69 | // ************************************************** 70 | 71 | // 72 | // Showdown namespace 73 | // 74 | var Showdown = {}; 75 | 76 | // 77 | // converter 78 | // 79 | // Wraps all "globals" so that the only thing 80 | // exposed is makeHtml(). 81 | // 82 | Showdown.converter = function() { 83 | 84 | // 85 | // Globals: 86 | // 87 | 88 | // Global hashes, used by various utility routines 89 | var g_urls; 90 | var g_titles; 91 | var g_html_blocks; 92 | 93 | // Used to track when we're inside an ordered or unordered list 94 | // (see _ProcessListItems() for details): 95 | var g_list_level = 0; 96 | 97 | 98 | this.makeHtml = function(text) { 99 | // 100 | // Main function. The order in which other subs are called here is 101 | // essential. Link and image substitutions need to happen before 102 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 103 | // and tags get encoded. 104 | // 105 | 106 | // Clear the global hashes. If we don't clear these, you get conflicts 107 | // from other articles when generating a page which contains more than 108 | // one article (e.g. an index page that shows the N most recent 109 | // articles): 110 | g_urls = new Array(); 111 | g_titles = new Array(); 112 | g_html_blocks = new Array(); 113 | 114 | // attacklab: Replace ~ with ~T 115 | // This lets us use tilde as an escape char to avoid md5 hashes 116 | // The choice of character is arbitray; anything that isn't 117 | // magic in Markdown will work. 118 | text = text.replace(/~/g,"~T"); 119 | 120 | // attacklab: Replace $ with ~D 121 | // RegExp interprets $ as a special character 122 | // when it's in a replacement string 123 | text = text.replace(/\$/g,"~D"); 124 | 125 | // Standardize line endings 126 | text = text.replace(/\r\n/g,"\n"); // DOS to Unix 127 | text = text.replace(/\r/g,"\n"); // Mac to Unix 128 | 129 | // Make sure text begins and ends with a couple of newlines: 130 | text = "\n\n" + text + "\n\n"; 131 | 132 | // Convert all tabs to spaces. 133 | text = _Detab(text); 134 | 135 | // Strip any lines consisting only of spaces and tabs. 136 | // This makes subsequent regexen easier to write, because we can 137 | // match consecutive blank lines with /\n+/ instead of something 138 | // contorted like /[ \t]*\n+/ . 139 | text = text.replace(/^[ \t]+$/mg,""); 140 | 141 | // Turn block-level HTML blocks into hash entries 142 | text = _HashHTMLBlocks(text); 143 | 144 | // Strip link definitions, store in hashes. 145 | text = _StripLinkDefinitions(text); 146 | 147 | text = _RunBlockGamut(text); 148 | 149 | text = _UnescapeSpecialChars(text); 150 | 151 | // attacklab: Restore dollar signs 152 | text = text.replace(/~D/g,"$$"); 153 | 154 | // attacklab: Restore tildes 155 | text = text.replace(/~T/g,"~"); 156 | 157 | // ** GFM ** Auto-link URLs and emails 158 | text = text.replace(/https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/g, function(wholeMatch,matchIndex){ 159 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 160 | if (left.match(/<[^>]+$/) && right.match(/^[^>]*>/)) {return wholeMatch} 161 | return "" + wholeMatch + ""; 162 | }); 163 | text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig, function(wholeMatch){return "" + wholeMatch + "";}); 164 | 165 | // ** GFM ** Auto-link sha1 if GitHub.nameWithOwner is defined 166 | text = text.replace(/[a-f0-9]{40}/ig, function(wholeMatch,matchIndex){ 167 | if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;} 168 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 169 | if (left.match(/@$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;} 170 | return "" + wholeMatch.substring(0,7) + ""; 171 | }); 172 | 173 | // ** GFM ** Auto-link user@sha1 if GitHub.nameWithOwner is defined 174 | text = text.replace(/([a-z0-9_\-+=.]+)@([a-f0-9]{40})/ig, function(wholeMatch,username,sha,matchIndex){ 175 | if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;} 176 | GitHub.repoName = GitHub.repoName || _GetRepoName() 177 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 178 | if (left.match(/\/$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;} 179 | return "" + username + "@" + sha.substring(0,7) + ""; 180 | }); 181 | 182 | // ** GFM ** Auto-link user/repo@sha1 183 | text = text.replace(/([a-z0-9_\-+=.]+\/[a-z0-9_\-+=.]+)@([a-f0-9]{40})/ig, function(wholeMatch,repo,sha){ 184 | return "" + repo + "@" + sha.substring(0,7) + ""; 185 | }); 186 | 187 | // ** GFM ** Auto-link #issue if GitHub.nameWithOwner is defined 188 | text = text.replace(/#([0-9]+)/ig, function(wholeMatch,issue,matchIndex){ 189 | if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;} 190 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 191 | if (left == "" || left.match(/[a-z0-9_\-+=.]$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;} 192 | return "" + wholeMatch + ""; 193 | }); 194 | 195 | // ** GFM ** Auto-link user#issue if GitHub.nameWithOwner is defined 196 | text = text.replace(/([a-z0-9_\-+=.]+)#([0-9]+)/ig, function(wholeMatch,username,issue,matchIndex){ 197 | if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;} 198 | GitHub.repoName = GitHub.repoName || _GetRepoName() 199 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 200 | if (left.match(/\/$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;} 201 | return "" + wholeMatch + ""; 202 | }); 203 | 204 | // ** GFM ** Auto-link user/repo#issue 205 | text = text.replace(/([a-z0-9_\-+=.]+\/[a-z0-9_\-+=.]+)#([0-9]+)/ig, function(wholeMatch,repo,issue){ 206 | return "" + wholeMatch + ""; 207 | }); 208 | 209 | return text; 210 | } 211 | 212 | 213 | var _GetRepoName = function() { 214 | return GitHub.nameWithOwner.match(/^.+\/(.+)$/)[1] 215 | } 216 | 217 | var _StripLinkDefinitions = function(text) { 218 | // 219 | // Strips link definitions from text, stores the URLs and titles in 220 | // hash references. 221 | // 222 | 223 | // Link defs are in the form: ^[id]: url "optional title" 224 | 225 | /* 226 | var text = text.replace(/ 227 | ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 228 | [ \t]* 229 | \n? // maybe *one* newline 230 | [ \t]* 231 | ? // url = $2 232 | [ \t]* 233 | \n? // maybe one newline 234 | [ \t]* 235 | (?: 236 | (\n*) // any lines skipped = $3 attacklab: lookbehind removed 237 | ["(] 238 | (.+?) // title = $4 239 | [")] 240 | [ \t]* 241 | )? // title is optional 242 | (?:\n+|$) 243 | /gm, 244 | function(){...}); 245 | */ 246 | var text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|\Z)/gm, 247 | function (wholeMatch,m1,m2,m3,m4) { 248 | m1 = m1.toLowerCase(); 249 | g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive 250 | if (m3) { 251 | // Oops, found blank lines, so it's not a title. 252 | // Put back the parenthetical statement we stole. 253 | return m3+m4; 254 | } else if (m4) { 255 | g_titles[m1] = m4.replace(/"/g,"""); 256 | } 257 | 258 | // Completely remove the definition from the text 259 | return ""; 260 | } 261 | ); 262 | 263 | return text; 264 | } 265 | 266 | 267 | var _HashHTMLBlocks = function(text) { 268 | // attacklab: Double up blank lines to reduce lookaround 269 | text = text.replace(/\n/g,"\n\n"); 270 | 271 | // Hashify HTML blocks: 272 | // We only want to do this for block-level HTML tags, such as headers, 273 | // lists, and tables. That's because we still want to wrap

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

282 | //
283 | // tags for inner block must be indented. 284 | //
285 | //
286 | // 287 | // The outermost tags must start at the left margin for this to match, and 288 | // the inner nested divs must be indented. 289 | // We need to do this before the next, more liberal match, because the next 290 | // match will start at the first `
` and stop at the first `
`. 291 | 292 | // attacklab: This regex can be expensive when it fails. 293 | /* 294 | var text = text.replace(/ 295 | ( // save in $1 296 | ^ // start of line (with /m) 297 | <($block_tags_a) // start tag = $2 298 | \b // word break 299 | // attacklab: hack around khtml/pcre bug... 300 | [^\r]*?\n // any number of lines, minimally matching 301 | // the matching end tag 302 | [ \t]* // trailing spaces/tabs 303 | (?=\n+) // followed by a newline 304 | ) // attacklab: there are sentinel newlines at end of document 305 | /gm,function(){...}}; 306 | */ 307 | 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); 308 | 309 | // 310 | // Now match more liberally, simply from `\n` to `\n` 311 | // 312 | 313 | /* 314 | var text = text.replace(/ 315 | ( // save in $1 316 | ^ // start of line (with /m) 317 | <($block_tags_b) // start tag = $2 318 | \b // word break 319 | // attacklab: hack around khtml/pcre bug... 320 | [^\r]*? // any number of lines, minimally matching 321 | .* // the matching end tag 322 | [ \t]* // trailing spaces/tabs 323 | (?=\n+) // followed by a newline 324 | ) // attacklab: there are sentinel newlines at end of document 325 | /gm,function(){...}}; 326 | */ 327 | 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); 328 | 329 | // Special case just for
. It was easier to make a special case than 330 | // to make the other regex more complicated. 331 | 332 | /* 333 | text = text.replace(/ 334 | ( // save in $1 335 | \n\n // Starting after a blank line 336 | [ ]{0,3} 337 | (<(hr) // start tag = $2 338 | \b // word break 339 | ([^<>])*? // 340 | \/?>) // the matching end tag 341 | [ \t]* 342 | (?=\n{2,}) // followed by a blank line 343 | ) 344 | /g,hashElement); 345 | */ 346 | text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,hashElement); 347 | 348 | // Special case for standalone HTML comments: 349 | 350 | /* 351 | text = text.replace(/ 352 | ( // save in $1 353 | \n\n // Starting after a blank line 354 | [ ]{0,3} // attacklab: g_tab_width - 1 355 | 358 | [ \t]* 359 | (?=\n{2,}) // followed by a blank line 360 | ) 361 | /g,hashElement); 362 | */ 363 | text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,hashElement); 364 | 365 | // PHP and ASP-style processor instructions ( and <%...%>) 366 | 367 | /* 368 | text = text.replace(/ 369 | (?: 370 | \n\n // Starting after a blank line 371 | ) 372 | ( // save in $1 373 | [ ]{0,3} // attacklab: g_tab_width - 1 374 | (?: 375 | <([?%]) // $2 376 | [^\r]*? 377 | \2> 378 | ) 379 | [ \t]* 380 | (?=\n{2,}) // followed by a blank line 381 | ) 382 | /g,hashElement); 383 | */ 384 | text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,hashElement); 385 | 386 | // attacklab: Undo double lines (see comment at top of this function) 387 | text = text.replace(/\n\n/g,"\n"); 388 | return text; 389 | } 390 | 391 | var hashElement = function(wholeMatch,m1) { 392 | var blockText = m1; 393 | 394 | // Undo double lines 395 | blockText = blockText.replace(/\n\n/g,"\n"); 396 | blockText = blockText.replace(/^\n/,""); 397 | 398 | // strip trailing blank lines 399 | blockText = blockText.replace(/\n+$/g,""); 400 | 401 | // Replace the element text with a marker ("~KxK" where x is its key) 402 | blockText = "\n\n~K" + (g_html_blocks.push(blockText)-1) + "K\n\n"; 403 | 404 | return blockText; 405 | }; 406 | 407 | var _RunBlockGamut = function(text) { 408 | // 409 | // These are all the transformations that form block-level 410 | // tags like paragraphs, headers, and list items. 411 | // 412 | text = _DoHeaders(text); 413 | 414 | // Do Horizontal Rules: 415 | var key = hashBlock("
"); 416 | text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key); 417 | text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key); 418 | text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key); 419 | 420 | text = _DoLists(text); 421 | text = _DoCodeBlocks(text); 422 | text = _DoBlockQuotes(text); 423 | 424 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 425 | // was to escape raw HTML in the original Markdown source. This time, 426 | // we're escaping the markup we've just created, so that we don't wrap 427 | //

tags around block-level tags. 428 | text = _HashHTMLBlocks(text); 429 | text = _FormParagraphs(text); 430 | 431 | return text; 432 | } 433 | 434 | 435 | var _RunSpanGamut = function(text) { 436 | // 437 | // These are all the transformations that occur *within* block-level 438 | // tags like paragraphs, headers, and list items. 439 | // 440 | 441 | text = _DoCodeSpans(text); 442 | text = _EscapeSpecialCharsWithinTagAttributes(text); 443 | text = _EncodeBackslashEscapes(text); 444 | 445 | // Process anchor and image tags. Images must come first, 446 | // because ![foo][f] looks like an anchor. 447 | text = _DoImages(text); 448 | text = _DoAnchors(text); 449 | 450 | // Make links out of things like `` 451 | // Must come after _DoAnchors(), because you can use < and > 452 | // delimiters in inline links like [this](). 453 | text = _DoAutoLinks(text); 454 | text = _EncodeAmpsAndAngles(text); 455 | text = _DoItalicsAndBold(text); 456 | 457 | // Do hard breaks: 458 | text = text.replace(/ +\n/g,"
\n"); 459 | 460 | return text; 461 | } 462 | 463 | var _EscapeSpecialCharsWithinTagAttributes = function(text) { 464 | // 465 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they 466 | // don't conflict with their use in Markdown for code, italics and strong. 467 | // 468 | 469 | // Build a regex to find HTML tags and comments. See Friedl's 470 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. 471 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi; 472 | 473 | text = text.replace(regex, function(wholeMatch) { 474 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g,"$1`"); 475 | tag = escapeCharacters(tag,"\\`*_"); 476 | return tag; 477 | }); 478 | 479 | return text; 480 | } 481 | 482 | var _DoAnchors = function(text) { 483 | // 484 | // Turn Markdown link shortcuts into XHTML tags. 485 | // 486 | // 487 | // First, handle reference-style links: [link text] [id] 488 | // 489 | 490 | /* 491 | text = text.replace(/ 492 | ( // wrap whole match in $1 493 | \[ 494 | ( 495 | (?: 496 | \[[^\]]*\] // allow brackets nested one level 497 | | 498 | [^\[] // or anything else 499 | )* 500 | ) 501 | \] 502 | 503 | [ ]? // one optional space 504 | (?:\n[ ]*)? // one optional newline followed by spaces 505 | 506 | \[ 507 | (.*?) // id = $3 508 | \] 509 | )()()()() // pad remaining backreferences 510 | /g,_DoAnchors_callback); 511 | */ 512 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeAnchorTag); 513 | 514 | // 515 | // Next, inline-style links: [link text](url "optional title") 516 | // 517 | 518 | /* 519 | text = text.replace(/ 520 | ( // wrap whole match in $1 521 | \[ 522 | ( 523 | (?: 524 | \[[^\]]*\] // allow brackets nested one level 525 | | 526 | [^\[\]] // or anything else 527 | ) 528 | ) 529 | \] 530 | \( // literal paren 531 | [ \t]* 532 | () // no id, so leave $3 empty 533 | ? // href = $4 534 | [ \t]* 535 | ( // $5 536 | (['"]) // quote char = $6 537 | (.*?) // Title = $7 538 | \6 // matching quote 539 | [ \t]* // ignore any spaces/tabs between closing quote and ) 540 | )? // title is optional 541 | \) 542 | ) 543 | /g,writeAnchorTag); 544 | */ 545 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag); 546 | 547 | // 548 | // Last, handle reference-style shortcuts: [link text] 549 | // These must come last in case you've also got [link test][1] 550 | // or [link test](/foo) 551 | // 552 | 553 | /* 554 | text = text.replace(/ 555 | ( // wrap whole match in $1 556 | \[ 557 | ([^\[\]]+) // link text = $2; can't contain '[' or ']' 558 | \] 559 | )()()()()() // pad rest of backreferences 560 | /g, writeAnchorTag); 561 | */ 562 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); 563 | 564 | return text; 565 | } 566 | 567 | var writeAnchorTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { 568 | if (m7 == undefined) m7 = ""; 569 | var whole_match = m1; 570 | var link_text = m2; 571 | var link_id = m3.toLowerCase(); 572 | var url = m4; 573 | var title = m7; 574 | 575 | if (url == "") { 576 | if (link_id == "") { 577 | // lower-case and turn embedded newlines into spaces 578 | link_id = link_text.toLowerCase().replace(/ ?\n/g," "); 579 | } 580 | url = "#"+link_id; 581 | 582 | if (g_urls[link_id] != undefined) { 583 | url = g_urls[link_id]; 584 | if (g_titles[link_id] != undefined) { 585 | title = g_titles[link_id]; 586 | } 587 | } 588 | else { 589 | if (whole_match.search(/\(\s*\)$/m)>-1) { 590 | // Special case for explicit empty url 591 | url = ""; 592 | } else { 593 | return whole_match; 594 | } 595 | } 596 | } 597 | 598 | url = escapeCharacters(url,"*_"); 599 | var result = ""; 608 | 609 | return result; 610 | } 611 | 612 | 613 | var _DoImages = function(text) { 614 | // 615 | // Turn Markdown image shortcuts into tags. 616 | // 617 | 618 | // 619 | // First, handle reference-style labeled images: ![alt text][id] 620 | // 621 | 622 | /* 623 | text = text.replace(/ 624 | ( // wrap whole match in $1 625 | !\[ 626 | (.*?) // alt text = $2 627 | \] 628 | 629 | [ ]? // one optional space 630 | (?:\n[ ]*)? // one optional newline followed by spaces 631 | 632 | \[ 633 | (.*?) // id = $3 634 | \] 635 | )()()()() // pad rest of backreferences 636 | /g,writeImageTag); 637 | */ 638 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeImageTag); 639 | 640 | // 641 | // Next, handle inline images: ![alt text](url "optional title") 642 | // Don't forget: encode * and _ 643 | 644 | /* 645 | text = text.replace(/ 646 | ( // wrap whole match in $1 647 | !\[ 648 | (.*?) // alt text = $2 649 | \] 650 | \s? // One optional whitespace character 651 | \( // literal paren 652 | [ \t]* 653 | () // no id, so leave $3 empty 654 | ? // src url = $4 655 | [ \t]* 656 | ( // $5 657 | (['"]) // quote char = $6 658 | (.*?) // title = $7 659 | \6 // matching quote 660 | [ \t]* 661 | )? // title is optional 662 | \) 663 | ) 664 | /g,writeImageTag); 665 | */ 666 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeImageTag); 667 | 668 | return text; 669 | } 670 | 671 | var writeImageTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { 672 | var whole_match = m1; 673 | var alt_text = m2; 674 | var link_id = m3.toLowerCase(); 675 | var url = m4; 676 | var title = m7; 677 | 678 | if (!title) title = ""; 679 | 680 | if (url == "") { 681 | if (link_id == "") { 682 | // lower-case and turn embedded newlines into spaces 683 | link_id = alt_text.toLowerCase().replace(/ ?\n/g," "); 684 | } 685 | url = "#"+link_id; 686 | 687 | if (g_urls[link_id] != undefined) { 688 | url = g_urls[link_id]; 689 | if (g_titles[link_id] != undefined) { 690 | title = g_titles[link_id]; 691 | } 692 | } 693 | else { 694 | return whole_match; 695 | } 696 | } 697 | 698 | alt_text = alt_text.replace(/"/g,"""); 699 | url = escapeCharacters(url,"*_"); 700 | var result = "\""" + _RunSpanGamut(m1) + "");}); 728 | 729 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, 730 | function(matchFound,m1){return hashBlock("

" + _RunSpanGamut(m1) + "

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

    Just type foo `bar` baz at the prompt.

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

    tags 1107 | // 1108 | 1109 | // Strip leading and trailing lines: 1110 | text = text.replace(/^\n+/g,""); 1111 | text = text.replace(/\n+$/g,""); 1112 | 1113 | var grafs = text.split(/\n{2,}/g); 1114 | var grafsOut = new Array(); 1115 | 1116 | // 1117 | // Wrap

    tags. 1118 | // 1119 | var end = grafs.length; 1120 | for (var i=0; i= 0) { 1125 | grafsOut.push(str); 1126 | } 1127 | else if (str.search(/\S/) >= 0) { 1128 | str = _RunSpanGamut(str); 1129 | str = str.replace(/\n/g,"
    "); // ** GFM ** 1130 | str = str.replace(/^([ \t]*)/g,"

    "); 1131 | str += "

    " 1132 | grafsOut.push(str); 1133 | } 1134 | 1135 | } 1136 | 1137 | // 1138 | // Unhashify HTML blocks 1139 | // 1140 | end = grafsOut.length; 1141 | for (var i=0; i= 0) { 1144 | var blockText = g_html_blocks[RegExp.$1]; 1145 | blockText = blockText.replace(/\$/g,"$$$$"); // Escape any dollar signs 1146 | grafsOut[i] = grafsOut[i].replace(/~K\d+K/,blockText); 1147 | } 1148 | } 1149 | 1150 | return grafsOut.join("\n\n"); 1151 | } 1152 | 1153 | 1154 | var _EncodeAmpsAndAngles = function(text) { 1155 | // Smart processing for ampersands and angle brackets that need to be encoded. 1156 | 1157 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1158 | // http://bumppo.net/projects/amputator/ 1159 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&"); 1160 | 1161 | // Encode naked <'s 1162 | text = text.replace(/<(?![a-z\/?\$!])/gi,"<"); 1163 | 1164 | return text; 1165 | } 1166 | 1167 | 1168 | var _EncodeBackslashEscapes = function(text) { 1169 | // 1170 | // Parameter: String. 1171 | // Returns: The string, with after processing the following backslash 1172 | // escape sequences. 1173 | // 1174 | 1175 | // attacklab: The polite way to do this is with the new 1176 | // escapeCharacters() function: 1177 | // 1178 | // text = escapeCharacters(text,"\\",true); 1179 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1180 | // 1181 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1182 | // as an optimization for Firefox. This function gets called a LOT. 1183 | 1184 | text = text.replace(/\\(\\)/g,escapeCharacters_callback); 1185 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g,escapeCharacters_callback); 1186 | return text; 1187 | } 1188 | 1189 | 1190 | var _DoAutoLinks = function(text) { 1191 | 1192 | text = text.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"
    $1"); 1193 | 1194 | // Email addresses: 1195 | 1196 | /* 1197 | text = text.replace(/ 1198 | < 1199 | (?:mailto:)? 1200 | ( 1201 | [-.\w]+ 1202 | \@ 1203 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1204 | ) 1205 | > 1206 | /gi, _DoAutoLinks_callback()); 1207 | */ 1208 | text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, 1209 | function(wholeMatch,m1) { 1210 | return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); 1211 | } 1212 | ); 1213 | 1214 | return text; 1215 | } 1216 | 1217 | 1218 | var _EncodeEmailAddress = function(addr) { 1219 | // 1220 | // Input: an email address, e.g. "foo@example.com" 1221 | // 1222 | // Output: the email address as a mailto link, with each character 1223 | // of the address encoded as either a decimal or hex entity, in 1224 | // the hopes of foiling most address harvesting spam bots. E.g.: 1225 | // 1226 | // foo 1228 | // @example.com 1229 | // 1230 | // Based on a filter by Matthew Wickline, posted to the BBEdit-Talk 1231 | // mailing list: 1232 | // 1233 | 1234 | // attacklab: why can't javascript speak hex? 1235 | function char2hex(ch) { 1236 | var hexDigits = '0123456789ABCDEF'; 1237 | var dec = ch.charCodeAt(0); 1238 | return(hexDigits.charAt(dec>>4) + hexDigits.charAt(dec&15)); 1239 | } 1240 | 1241 | var encode = [ 1242 | function(ch){return "&#"+ch.charCodeAt(0)+";";}, 1243 | function(ch){return "&#x"+char2hex(ch)+";";}, 1244 | function(ch){return ch;} 1245 | ]; 1246 | 1247 | addr = "mailto:" + addr; 1248 | 1249 | addr = addr.replace(/./g, function(ch) { 1250 | if (ch == "@") { 1251 | // this *must* be encoded. I insist. 1252 | ch = encode[Math.floor(Math.random()*2)](ch); 1253 | } else if (ch !=":") { 1254 | // leave ':' alone (to spot mailto: later) 1255 | var r = Math.random(); 1256 | // roughly 10% raw, 45% hex, 45% dec 1257 | ch = ( 1258 | r > .9 ? encode[2](ch) : 1259 | r > .45 ? encode[1](ch) : 1260 | encode[0](ch) 1261 | ); 1262 | } 1263 | return ch; 1264 | }); 1265 | 1266 | addr = "" + addr + ""; 1267 | addr = addr.replace(/">.+:/g,"\">"); // strip the mailto: from the visible part 1268 | 1269 | return addr; 1270 | } 1271 | 1272 | 1273 | var _UnescapeSpecialChars = function(text) { 1274 | // 1275 | // Swap back in all the special characters we've hidden. 1276 | // 1277 | text = text.replace(/~E(\d+)E/g, 1278 | function(wholeMatch,m1) { 1279 | var charCodeToReplace = parseInt(m1); 1280 | return String.fromCharCode(charCodeToReplace); 1281 | } 1282 | ); 1283 | return text; 1284 | } 1285 | 1286 | 1287 | var _Outdent = function(text) { 1288 | // 1289 | // Remove one level of line-leading tabs or spaces 1290 | // 1291 | 1292 | // attacklab: hack around Konqueror 3.5.4 bug: 1293 | // "----------bug".replace(/^-/g,"") == "bug" 1294 | 1295 | text = text.replace(/^(\t|[ ]{1,4})/gm,"~0"); // attacklab: g_tab_width 1296 | 1297 | // attacklab: clean up hack 1298 | text = text.replace(/~0/g,"") 1299 | 1300 | return text; 1301 | } 1302 | 1303 | var _Detab = function(text) { 1304 | // attacklab: Detab's completely rewritten for speed. 1305 | // In perl we could fix it by anchoring the regexp with \G. 1306 | // In javascript we're less fortunate. 1307 | 1308 | // expand first n-1 tabs 1309 | text = text.replace(/\t(?=\t)/g," "); // attacklab: g_tab_width 1310 | 1311 | // replace the nth with two sentinels 1312 | text = text.replace(/\t/g,"~A~B"); 1313 | 1314 | // use the sentinel to anchor our regex so it doesn't explode 1315 | text = text.replace(/~B(.+?)~A/g, 1316 | function(wholeMatch,m1,m2) { 1317 | var leadingText = m1; 1318 | var numSpaces = 4 - leadingText.length % 4; // attacklab: g_tab_width 1319 | 1320 | // there *must* be a better way to do this: 1321 | for (var i=0; i